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";
9pub const PLATFORM_ACTION_GRAPH: &str = include_str!("../fixtures/v2/platform-action-graph.json");
10#[cfg(any(test, feature = "fixtures"))]
11#[doc(hidden)]
12pub const PLATFORM_SERVICE_STATUS_FIXTURE: &str =
13    include_str!("../fixtures/v2/platform-status.json");
14#[cfg(any(test, feature = "fixtures"))]
15#[doc(hidden)]
16pub const PLATFORM_CAPABILITIES_FIXTURE: &str =
17    include_str!("../fixtures/v2/platform-capabilities.json");
18#[cfg(any(test, feature = "fixtures"))]
19#[doc(hidden)]
20pub const PLATFORM_ASSETS_FIXTURE: &str = include_str!("../fixtures/v2/assets.json");
21#[cfg(any(test, feature = "fixtures"))]
22#[doc(hidden)]
23pub const PLATFORM_SWAP_QUOTE_FIXTURE: &str = include_str!("../fixtures/v2/swap-quote.json");
24#[cfg(any(test, feature = "fixtures"))]
25#[doc(hidden)]
26pub const PLATFORM_MARKETS_FIXTURE: &str = include_str!("../fixtures/v2/markets.json");
27#[cfg(any(test, feature = "fixtures"))]
28#[doc(hidden)]
29pub const PLATFORM_BOOK_FIXTURE: &str = include_str!("../fixtures/v2/book.json");
30#[cfg(any(test, feature = "fixtures"))]
31#[doc(hidden)]
32pub const PLATFORM_BBO_FIXTURE: &str = include_str!("../fixtures/v2/bbo.json");
33#[cfg(any(test, feature = "fixtures"))]
34#[doc(hidden)]
35pub const PLATFORM_FEES_FIXTURE: &str = include_str!("../fixtures/v2/fees.json");
36#[cfg(any(test, feature = "fixtures"))]
37#[doc(hidden)]
38pub const PLATFORM_STATUS_FIXTURE: &str = include_str!("../fixtures/v2/status.json");
39#[cfg(any(test, feature = "fixtures"))]
40#[doc(hidden)]
41pub const PLATFORM_CANDLES_FIXTURE: &str = include_str!("../fixtures/v2/candles.json");
42#[cfg(any(test, feature = "fixtures"))]
43#[doc(hidden)]
44pub const PLATFORM_MARK_FIXTURE: &str = include_str!("../fixtures/v2/mark.json");
45#[cfg(any(test, feature = "fixtures"))]
46#[doc(hidden)]
47pub const PLATFORM_EXECUTION_STATUS_FIXTURE: &str =
48    include_str!("../fixtures/v2/execution-status.json");
49#[cfg(any(test, feature = "fixtures"))]
50#[doc(hidden)]
51pub const PLATFORM_TWAPS_FIXTURE: &str = include_str!("../fixtures/v2/twaps.json");
52#[cfg(any(test, feature = "fixtures"))]
53#[doc(hidden)]
54pub const PLATFORM_TWAP_CHALLENGE_FIXTURE: &str =
55    include_str!("../fixtures/v2/twap-challenge.json");
56#[cfg(any(test, feature = "fixtures"))]
57#[doc(hidden)]
58pub const PLATFORM_TWAP_PREPARE_FIXTURE: &str = include_str!("../fixtures/v2/twap-prepare.json");
59#[cfg(any(test, feature = "fixtures"))]
60#[doc(hidden)]
61pub const PLATFORM_TWAP_SUBMIT_FIXTURE: &str = include_str!("../fixtures/v2/twap-submit.json");
62#[cfg(any(test, feature = "fixtures"))]
63#[doc(hidden)]
64pub const PLATFORM_PORTFOLIO_HISTORY_FIXTURE: &str =
65    include_str!("../fixtures/v2/portfolio-history.json");
66#[cfg(any(test, feature = "fixtures"))]
67#[doc(hidden)]
68pub const PLATFORM_PORTFOLIO_FIXTURE: &str = include_str!("../fixtures/v2/portfolio.json");
69#[cfg(any(test, feature = "fixtures"))]
70#[doc(hidden)]
71pub const PLATFORM_REWARDS_FIXTURE: &str = include_str!("../fixtures/v2/rewards.json");
72#[cfg(any(test, feature = "fixtures"))]
73#[doc(hidden)]
74pub const PLATFORM_REFERRALS_FIXTURE: &str = include_str!("../fixtures/v2/referrals.json");
75#[cfg(any(test, feature = "fixtures"))]
76#[doc(hidden)]
77pub const PLATFORM_REFERRAL_LINK_FIXTURE: &str = include_str!("../fixtures/v2/referral-link.json");
78#[cfg(any(test, feature = "fixtures"))]
79#[doc(hidden)]
80pub const PLATFORM_REFERRAL_CLAIM_FIXTURE: &str =
81    include_str!("../fixtures/v2/referral-claim.json");
82#[cfg(any(test, feature = "fixtures"))]
83#[doc(hidden)]
84pub const PLATFORM_VAULT_STATUS_FIXTURE: &str = include_str!("../fixtures/v2/vault-status.json");
85#[cfg(any(test, feature = "fixtures"))]
86#[doc(hidden)]
87pub const PLATFORM_VAULT_PAUSE_PREPARE_FIXTURE: &str =
88    include_str!("../fixtures/v2/vault-pause-prepare.json");
89#[cfg(any(test, feature = "fixtures"))]
90#[doc(hidden)]
91pub const PLATFORM_VAULT_SETUP_PREPARE_FIXTURE: &str =
92    include_str!("../fixtures/v2/vault-setup-prepare.json");
93pub const PLATFORM_VAULT_DELEGATE_PREPARE_FIXTURE: &str =
94    include_str!("../fixtures/v2/vault-delegate-prepare.json");
95pub const PLATFORM_VAULT_POLICY_PREPARE_FIXTURE: &str =
96    include_str!("../fixtures/v2/vault-policy-prepare.json");
97pub const PLATFORM_VAULT_DEPOSIT_PREPARE_FIXTURE: &str =
98    include_str!("../fixtures/v2/vault-deposit-prepare.json");
99pub const PLATFORM_VAULT_WITHDRAW_PREPARE_FIXTURE: &str =
100    include_str!("../fixtures/v2/vault-withdraw-prepare.json");
101pub const PLATFORM_VAULT_SUBMIT_FIXTURE: &str = include_str!("../fixtures/v2/vault-submit.json");
102#[cfg(any(test, feature = "fixtures"))]
103#[doc(hidden)]
104pub const PLATFORM_BUGS_FIXTURE: &str = include_str!("../fixtures/v2/bugs.json");
105#[cfg(any(test, feature = "fixtures"))]
106#[doc(hidden)]
107pub const PLATFORM_BUG_SUBMIT_FIXTURE: &str = include_str!("../fixtures/v2/bug-submit.json");
108#[cfg(any(test, feature = "fixtures"))]
109#[doc(hidden)]
110pub const PLATFORM_TRADES_FIXTURE: &str = include_str!("../fixtures/v2/trades.json");
111#[cfg(any(test, feature = "fixtures"))]
112#[doc(hidden)]
113pub const PLATFORM_ACCOUNT_FIXTURE: &str = include_str!("../fixtures/v2/account.json");
114#[cfg(any(test, feature = "fixtures"))]
115#[doc(hidden)]
116pub const PLATFORM_MAKER_REPUTATION_FIXTURE: &str =
117    include_str!("../fixtures/v2/maker-reputation.json");
118#[cfg(any(test, feature = "fixtures"))]
119#[doc(hidden)]
120pub const PLATFORM_MAKER_STATUS_FIXTURE: &str = include_str!("../fixtures/v2/maker-status.json");
121#[cfg(any(test, feature = "fixtures"))]
122#[doc(hidden)]
123pub const PLATFORM_MAKER_STREAM_FIXTURE: &str = include_str!("../fixtures/v2/maker-stream.json");
124#[cfg(any(test, feature = "fixtures"))]
125#[doc(hidden)]
126pub const PLATFORM_TWAP_STREAM_FIXTURE: &str = include_str!("../fixtures/v2/twap-stream.json");
127#[cfg(any(test, feature = "fixtures"))]
128#[doc(hidden)]
129pub const PLATFORM_EXECUTION_STREAM_FIXTURE: &str =
130    include_str!("../fixtures/v2/execution-stream.json");
131#[cfg(any(test, feature = "fixtures"))]
132#[doc(hidden)]
133pub const PLATFORM_ORDER_CHALLENGE_FIXTURE: &str =
134    include_str!("../fixtures/v2/order-challenge.json");
135#[cfg(any(test, feature = "fixtures"))]
136#[doc(hidden)]
137pub const PLATFORM_ORDER_PREPARE_FIXTURE: &str = include_str!("../fixtures/v2/order-prepare.json");
138#[cfg(any(test, feature = "fixtures"))]
139#[doc(hidden)]
140pub const PLATFORM_ORDER_SUBMIT_FIXTURE: &str = include_str!("../fixtures/v2/order-submit.json");
141#[cfg(any(test, feature = "fixtures"))]
142#[doc(hidden)]
143pub const PLATFORM_ORDER_STATUS_FIXTURE: &str = include_str!("../fixtures/v2/order-status.json");
144
145#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
146#[serde(rename_all = "snake_case")]
147pub enum PermissionSource {
148    ExternalAgentOwner,
149}
150
151#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
152#[serde(rename_all = "snake_case")]
153pub enum SigningLocation {
154    External,
155}
156
157#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
158#[serde(deny_unknown_fields)]
159pub struct PlatformAuthority {
160    pub permission_source: PermissionSource,
161    pub signing_location: SigningLocation,
162    pub accepts_private_keys: bool,
163}
164
165#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
166#[serde(rename_all = "snake_case")]
167pub enum PlatformTransport {
168    Http,
169    Websocket,
170    Mcp,
171}
172
173#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
174#[serde(rename_all = "snake_case")]
175pub enum PlatformMarketState {
176    Active,
177    ReadOnly,
178    QuoteOnly,
179    CancelOnly,
180    Paused,
181    Warming,
182    Degraded,
183    Unavailable,
184}
185
186#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
187#[serde(rename_all = "snake_case")]
188pub enum PlatformOrderState {
189    Created,
190    Accepted,
191    Open,
192    PartiallyFilled,
193    Filled,
194    CancelPending,
195    Cancelled,
196    Expired,
197    Rejected,
198}
199
200#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
201#[serde(rename_all = "snake_case")]
202pub enum PlatformSettlementState {
203    NotApplicable,
204    Pending,
205    Confirmed,
206    Failed,
207}
208
209#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
210#[serde(rename_all = "snake_case")]
211pub enum PlatformPublicErrorCode {
212    InvalidRequest,
213    UnsupportedCapability,
214    MarketUnavailable,
215    MarketWarming,
216    QuoteUnavailable,
217    QuoteExpired,
218    PriceBoundFailed,
219    InsufficientBalance,
220    PolicyRejected,
221    SessionExpired,
222    SequenceConflict,
223    DuplicateClientId,
224    OrderRejected,
225    OrderNotFound,
226    CancelTooLate,
227    SelfTradePrevented,
228    DeadManExpired,
229    RateLimited,
230    TemporarilyUnavailable,
231    SubmissionAmbiguous,
232    SettlementPending,
233    SettlementFailed,
234}
235
236/// Exact asset amount. Public money never crosses the contract as a float.
237#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
238#[serde(deny_unknown_fields)]
239pub struct ExactAmount {
240    pub asset_id: String,
241    pub atoms: String,
242}
243
244/// Sequence metadata shared by all recoverable state streams.
245#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
246#[serde(deny_unknown_fields)]
247pub struct SequenceEnvelope {
248    pub stream_id: String,
249    pub sequence: String,
250    pub previous_sequence: Option<String>,
251    pub server_time_ms: u64,
252    #[serde(skip_serializing_if = "Option::is_none")]
253    pub snapshot_id: Option<String>,
254}
255
256#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
257#[serde(deny_unknown_fields)]
258pub struct PageRequest {
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub cursor: Option<String>,
261    #[serde(skip_serializing_if = "Option::is_none")]
262    pub limit: Option<u32>,
263}
264
265#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
266#[serde(deny_unknown_fields)]
267pub struct PageInfo {
268    pub next_cursor: Option<String>,
269    pub has_more: bool,
270}
271
272#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
273#[serde(deny_unknown_fields)]
274pub struct PublicOperationError {
275    pub code: PlatformPublicErrorCode,
276    pub message: String,
277    pub retryable: bool,
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub retry_after_ms: Option<u64>,
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub operation_id: Option<String>,
282}
283
284/// One operation currently callable through the live v2 gateway.
285#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
286#[serde(deny_unknown_fields)]
287pub struct LivePlatformCapability {
288    pub id: String,
289    pub risk: CapabilityRisk,
290    pub required_scope: String,
291    pub transports: Vec<PlatformTransport>,
292    pub mcp_exposure: McpExposure,
293}
294
295/// Operations currently available to the client.
296#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
297#[serde(deny_unknown_fields)]
298pub struct PlatformDiscoveryResponse {
299    pub schema_version: u16,
300    pub contract_version: String,
301    pub server_time_ms: u64,
302    pub authority: PlatformAuthority,
303    pub capabilities: Vec<LivePlatformCapability>,
304}
305
306#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
307#[serde(rename_all = "snake_case")]
308pub enum PlatformServiceState {
309    Operational,
310    Degraded,
311}
312
313/// Product-level readiness without leaking private implementation details.
314#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
315#[serde(deny_unknown_fields)]
316pub struct PlatformServiceStatusResponse {
317    pub schema_version: u16,
318    pub contract_version: String,
319    pub server_time_ms: u64,
320    pub status: PlatformServiceState,
321    pub available_operations: u32,
322}
323
324#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
325#[serde(rename_all = "snake_case")]
326pub enum PlatformActionKind {
327    Discovery,
328    Read,
329    Prepare,
330    ExternalSignature,
331    Submit,
332    Receipt,
333    Stream,
334}
335
336#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
337#[serde(deny_unknown_fields)]
338pub struct PlatformGraphRelation {
339    pub from: String,
340    pub to: String,
341    pub kind: String,
342}
343
344#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
345#[serde(deny_unknown_fields)]
346pub struct PlatformGraphModule {
347    pub id: String,
348    pub client_property: String,
349    pub capability_ids: Vec<String>,
350}
351
352#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
353#[serde(deny_unknown_fields)]
354pub struct PlatformOperationTransport {
355    pub transport: PlatformTransport,
356    #[serde(skip_serializing_if = "Option::is_none")]
357    pub method: Option<String>,
358    #[serde(skip_serializing_if = "Option::is_none")]
359    pub path: Option<String>,
360    #[serde(skip_serializing_if = "Option::is_none")]
361    pub tool: Option<String>,
362}
363
364#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
365#[serde(deny_unknown_fields)]
366pub struct PlatformOperation {
367    pub id: String,
368    pub capability_id: String,
369    pub kind: PlatformActionKind,
370    pub summary: String,
371    pub transports: Vec<PlatformOperationTransport>,
372    pub available: bool,
373}
374
375#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
376#[serde(deny_unknown_fields)]
377pub struct PlatformWorkflowNode {
378    pub id: String,
379    pub kind: PlatformActionKind,
380    pub capability_id: Option<String>,
381    pub operation_ids: Vec<String>,
382    pub available: bool,
383}
384
385#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
386#[serde(deny_unknown_fields)]
387pub struct PlatformWorkflowEdge {
388    pub from: String,
389    pub to: String,
390    pub condition: String,
391}
392
393#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
394#[serde(deny_unknown_fields)]
395pub struct PlatformWorkflow {
396    pub id: String,
397    pub entry_node: String,
398    pub nodes: Vec<PlatformWorkflowNode>,
399    pub edges: Vec<PlatformWorkflowEdge>,
400}
401
402/// Complete customer-safe product graph. Static package support is projected
403/// against live capability discovery before this response is served.
404#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
405#[serde(deny_unknown_fields)]
406pub struct PlatformActionGraphResponse {
407    pub schema_version: u16,
408    pub contract_version: String,
409    pub graph_version: String,
410    pub entry_operation_id: String,
411    pub authority: PlatformAuthority,
412    pub entities: Vec<String>,
413    pub relations: Vec<PlatformGraphRelation>,
414    pub modules: Vec<PlatformGraphModule>,
415    pub operations: Vec<PlatformOperation>,
416    pub workflows: Vec<PlatformWorkflow>,
417}
418
419impl PlatformActionGraphResponse {
420    pub fn foundation() -> Self {
421        serde_json::from_str(PLATFORM_ACTION_GRAPH)
422            .expect("embedded platform action graph must be valid")
423    }
424
425    /// Live discovery is the authority. Package support alone never makes a
426    /// callable operation or workflow node available.
427    pub fn project_availability(
428        &mut self,
429        live_capability_ids: &std::collections::BTreeSet<String>,
430    ) {
431        for operation in &mut self.operations {
432            operation.available = live_capability_ids.contains(&operation.capability_id);
433        }
434        for workflow in &mut self.workflows {
435            for node in &mut workflow.nodes {
436                node.available = node
437                    .capability_id
438                    .as_ref()
439                    .is_none_or(|capability_id| live_capability_ids.contains(capability_id));
440            }
441        }
442    }
443}
444
445#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
446#[serde(rename_all = "snake_case")]
447pub enum PlatformNetwork {
448    Solana,
449}
450
451/// Asset identity used by ordinary SDK operations.
452#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
453#[serde(deny_unknown_fields)]
454pub struct PlatformAsset {
455    pub asset_id: String,
456    pub symbol: String,
457    pub name: String,
458    pub decimals: u8,
459    #[serde(skip_serializing_if = "Option::is_none")]
460    pub logo_url: Option<String>,
461    pub network: PlatformNetwork,
462}
463
464#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
465#[serde(deny_unknown_fields)]
466pub struct PlatformAssetsResponse {
467    pub schema_version: u16,
468    pub contract_version: String,
469    pub server_time_ms: u64,
470    pub assets: Vec<PlatformAsset>,
471    pub page: PageInfo,
472}
473
474/// Exact-input asset swap request. Asset identifiers come from
475/// [`PlatformAssetsResponse`]; implementation-specific identifiers are not
476/// part of this contract.
477#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
478#[serde(deny_unknown_fields)]
479pub struct PlatformSwapQuoteRequest {
480    pub input_asset_id: String,
481    pub output_asset_id: String,
482    pub amount_in_atoms: String,
483    #[serde(default)]
484    pub maximum_tolerance_bps: u16,
485}
486
487/// Short-lived customer economics for an exact-input asset swap.
488#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
489#[serde(deny_unknown_fields)]
490pub struct PlatformSwapQuoteResponse {
491    pub schema_version: u16,
492    pub contract_version: String,
493    pub quote_id: String,
494    pub server_time_ms: u64,
495    pub expires_at_ms: u64,
496    pub input_asset_id: String,
497    pub output_asset_id: String,
498    pub amount_in_atoms: String,
499    pub amount_in_consumed_atoms: String,
500    pub amount_out_atoms: String,
501    pub minimum_output_atoms: String,
502    pub input_fee_atoms: String,
503    pub output_fee_atoms: String,
504    pub maximum_tolerance_bps: u16,
505    pub reference_price: String,
506    pub price_impact_pct: String,
507    pub provider: String,
508}
509
510#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
511#[serde(rename_all = "snake_case")]
512pub enum PlatformMarketAction {
513    Quote,
514    ExecuteImmediate,
515    PlaceOrder,
516    ScheduleTwap,
517}
518
519/// Stable market metadata for public SDK operations.
520#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
521#[serde(deny_unknown_fields)]
522pub struct PlatformMarket {
523    pub market_id: String,
524    pub label: String,
525    pub base_asset_id: String,
526    pub quote_asset_id: String,
527    pub status: PlatformMarketState,
528    pub available_actions: Vec<PlatformMarketAction>,
529}
530
531#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
532#[serde(deny_unknown_fields)]
533pub struct PlatformMarketsResponse {
534    pub schema_version: u16,
535    pub contract_version: String,
536    pub server_time_ms: u64,
537    pub markets: Vec<PlatformMarket>,
538    pub page: PageInfo,
539}
540
541#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
542#[serde(deny_unknown_fields)]
543pub struct PlatformBookLevel {
544    /// Quote atoms per whole base unit, encoded as an unsigned decimal string.
545    pub price_atoms: String,
546    /// Available base quantity in base atoms, encoded as an unsigned decimal string.
547    pub size_atoms: String,
548}
549
550#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
551#[serde(rename_all = "snake_case")]
552pub enum PlatformBookSide {
553    Bid,
554    Ask,
555}
556
557#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
558#[serde(deny_unknown_fields)]
559pub struct PlatformBookChange {
560    pub side: PlatformBookSide,
561    pub price_atoms: String,
562    /// Zero removes the price level.
563    pub size_atoms: String,
564}
565
566#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
567#[serde(deny_unknown_fields)]
568pub struct PlatformBookSnapshotResponse {
569    pub schema_version: u16,
570    pub contract_version: String,
571    pub market_id: String,
572    pub stream_id: String,
573    pub sequence: String,
574    pub server_time_ms: u64,
575    pub snapshot_id: String,
576    pub bids: Vec<PlatformBookLevel>,
577    pub asks: Vec<PlatformBookLevel>,
578}
579
580#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
581#[serde(deny_unknown_fields)]
582pub struct PlatformBestBidAskResponse {
583    pub schema_version: u16,
584    pub contract_version: String,
585    pub market_id: String,
586    pub stream_id: String,
587    pub sequence: String,
588    pub server_time_ms: u64,
589    pub best_bid: Option<PlatformBookLevel>,
590    pub best_ask: Option<PlatformBookLevel>,
591}
592
593#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
594#[serde(deny_unknown_fields)]
595pub struct PlatformFeeScheduleResponse {
596    pub schema_version: u16,
597    pub contract_version: String,
598    pub market_id: String,
599    pub server_time_ms: u64,
600    pub passive_maker_fee_bps: u16,
601    pub maximum_immediate_execution_fee_bps: u16,
602    pub book_prices_include_trading_fees: bool,
603    pub exact_fee_returned_by_quote: bool,
604}
605
606#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
607#[serde(deny_unknown_fields)]
608pub struct PlatformMarketStatusResponse {
609    pub schema_version: u16,
610    pub contract_version: String,
611    pub market_id: String,
612    pub server_time_ms: u64,
613    pub status: PlatformMarketState,
614    pub tick_size_atoms: String,
615    /// Smallest accepted base-asset quantity. Strata orders are atom-denominated,
616    /// so this is `1`; it is a size, never a price or `Market.base_lot_size`.
617    pub minimum_order_size_atoms: String,
618}
619
620/// Decimal prices are strings so no SDK boundary silently rounds money.
621#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
622#[serde(deny_unknown_fields)]
623pub struct PlatformCandle {
624    pub started_at_ms: u64,
625    pub open_price: String,
626    pub high_price: String,
627    pub low_price: String,
628    pub close_price: String,
629}
630
631#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
632#[serde(deny_unknown_fields)]
633pub struct PlatformCandlesResponse {
634    pub schema_version: u16,
635    pub contract_version: String,
636    pub market_id: String,
637    pub server_time_ms: u64,
638    pub resolution_seconds: u32,
639    pub candles: Vec<PlatformCandle>,
640}
641
642#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
643#[serde(deny_unknown_fields)]
644pub struct PlatformMarkResponse {
645    pub schema_version: u16,
646    pub contract_version: String,
647    pub market_id: String,
648    pub server_time_ms: u64,
649    pub price_atoms_per_base_unit: Option<String>,
650    pub quote_decimals: u8,
651    pub stale: bool,
652    pub age_ms: Option<u64>,
653}
654
655#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
656#[serde(rename_all = "snake_case")]
657pub enum PlatformExecutionState {
658    Prepared,
659    Confirmed,
660}
661
662/// Recoverable immediate-execution receipt. Confirmed rows are journalled and
663/// survive a market-service restart.
664#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
665#[serde(deny_unknown_fields)]
666pub struct PlatformExecutionStatusResponse {
667    pub schema_version: u16,
668    pub contract_version: String,
669    pub execution_id: String,
670    pub market_id: String,
671    pub status: PlatformExecutionState,
672    pub signature: Option<String>,
673    pub settlement: PlatformSettlementState,
674    pub updated_at_ms: u64,
675}
676
677/// One watched immediate execution as the stream sees it: the same fields as
678/// the recoverable HTTP receipt without the envelope.
679#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
680#[serde(deny_unknown_fields)]
681pub struct PlatformExecutionRow {
682    pub execution_id: String,
683    pub market_id: String,
684    pub status: PlatformExecutionState,
685    pub signature: Option<String>,
686    pub settlement: PlatformSettlementState,
687    pub updated_at_ms: u64,
688}
689
690/// Client frame for the execution stream: watch one or more opaque execution
691/// handles issued by `execution.prepare` in this market.
692#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
693#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
694pub enum PlatformExecutionCommand {
695    Watch { execution_ids: Vec<String> },
696}
697
698/// Sequenced execution stream (`execution.stream`) for one market. The client
699/// opens the socket and sends a `watch` frame; the server answers with one
700/// `executions_snapshot` for the watched handles, then `execution_update`
701/// whenever a watched execution is prepared, confirmed on chain, or expires
702/// unconfirmed, `execution_unknown` for handles this market never issued or
703/// no longer remembers, and heartbeats. Later `watch` frames add handles and
704/// produce update/unknown events for them.
705#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
706#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
707pub enum PlatformExecutionEvent {
708    ExecutionsSnapshot {
709        schema_version: u16,
710        contract_version: String,
711        market_id: String,
712        stream_id: String,
713        sequence: String,
714        server_time_ms: u64,
715        executions: Vec<PlatformExecutionRow>,
716        unknown_execution_ids: Vec<String>,
717    },
718    ExecutionUpdate {
719        schema_version: u16,
720        contract_version: String,
721        market_id: String,
722        stream_id: String,
723        sequence: String,
724        previous_sequence: String,
725        server_time_ms: u64,
726        execution: PlatformExecutionRow,
727    },
728    ExecutionExpired {
729        schema_version: u16,
730        contract_version: String,
731        market_id: String,
732        stream_id: String,
733        sequence: String,
734        previous_sequence: String,
735        server_time_ms: u64,
736        execution_id: String,
737    },
738    ExecutionUnknown {
739        schema_version: u16,
740        contract_version: String,
741        market_id: String,
742        stream_id: String,
743        sequence: String,
744        previous_sequence: String,
745        server_time_ms: u64,
746        execution_id: String,
747    },
748    Heartbeat {
749        schema_version: u16,
750        contract_version: String,
751        market_id: String,
752        stream_id: String,
753        sequence: String,
754        previous_sequence: String,
755        server_time_ms: u64,
756    },
757}
758
759#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
760#[serde(rename_all = "snake_case")]
761pub enum PlatformTwapState {
762    Active,
763    Completed,
764    Cancelled,
765}
766
767#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
768#[serde(deny_unknown_fields)]
769pub struct PlatformTwapFill {
770    pub fill_id: String,
771    pub size_atoms: String,
772    pub price_atoms: String,
773    pub gross_quote_atoms: String,
774    pub base_fee_atoms: String,
775    pub quote_fee_atoms: String,
776    pub signature: Option<String>,
777    pub observed_at_ms: u64,
778}
779
780#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
781#[serde(deny_unknown_fields)]
782pub struct PlatformTwap {
783    pub twap_id: String,
784    pub side: PlatformTradeSide,
785    pub status: PlatformTwapState,
786    pub slices_total: u16,
787    pub slices_executed: u16,
788    pub interval_slots: u32,
789    pub maximum_tolerance_bps: u16,
790    pub limit_price_atoms: String,
791    pub total_size_atoms: String,
792    pub executed_size_atoms: String,
793    pub gross_quote_executed_atoms: String,
794    pub complete_execution_value: bool,
795    pub created_at_ms: u64,
796    pub completed_at_ms: Option<u64>,
797    pub placed_signature: Option<String>,
798    pub terminal_signature: Option<String>,
799    pub fills: Vec<PlatformTwapFill>,
800}
801
802#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
803#[serde(deny_unknown_fields)]
804pub struct PlatformTwapsResponse {
805    pub schema_version: u16,
806    pub contract_version: String,
807    pub market_id: String,
808    pub wallet_address: String,
809    pub server_time_ms: u64,
810    pub twaps: Vec<PlatformTwap>,
811}
812
813#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
814#[serde(rename_all = "snake_case")]
815pub enum PlatformTwapControlAction {
816    Place,
817    Cancel,
818}
819
820/// Request exact authorization bytes for one Vault-owned TWAP action. The
821/// external owner chooses the session signer; Strata never receives its key.
822#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
823#[serde(tag = "action", rename_all = "snake_case", deny_unknown_fields)]
824pub enum PlatformTwapChallengeRequest {
825    Place {
826        owner_wallet: String,
827        session_public_key: String,
828        side: PlatformTradeSide,
829        total_size_atoms: String,
830        slices_total: u16,
831        maximum_tolerance_bps: u16,
832        /// Slots between slices. Slot time is a cluster parameter (400 ms
833        /// today, stepping down to 200 ms under SIMD-0525), so a schedule
834        /// expressed in slots runs faster in wall time as slots shorten.
835        interval_slots: u32,
836        limit_price_atoms: String,
837    },
838    Cancel {
839        owner_wallet: String,
840        session_public_key: String,
841        twap_id: String,
842    },
843}
844
845#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
846#[serde(deny_unknown_fields)]
847pub struct PlatformTwapChallengeResponse {
848    pub schema_version: u16,
849    pub contract_version: String,
850    pub challenge_id: String,
851    pub market_id: String,
852    pub action: PlatformTwapControlAction,
853    pub twap_id: String,
854    pub authorization_payload_base64: String,
855    pub server_time_ms: u64,
856    pub expires_at_ms: u64,
857}
858
859/// A prepared TWAP challenge, signed: the two-step path.
860#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
861#[serde(deny_unknown_fields)]
862pub struct PlatformTwapPrepareAuthorization {
863    pub challenge_id: String,
864    /// Base58 Ed25519 signature over `authorization_payload_base64`.
865    pub authorization_signature: String,
866}
867
868/// Prepare a TWAP-control transaction: a signed challenge (`Authorized`) or
869/// the action itself (`Direct`, one signature — the transaction signature is
870/// the authorization). The response is identical.
871#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
872#[serde(untagged)]
873pub enum PlatformTwapPrepareRequest {
874    Authorized(PlatformTwapPrepareAuthorization),
875    Direct(PlatformTwapChallengeRequest),
876}
877
878#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
879#[serde(deny_unknown_fields)]
880pub struct PlatformTwapPrepareResponse {
881    pub schema_version: u16,
882    pub contract_version: String,
883    pub twap_control_id: String,
884    pub market_id: String,
885    pub action: PlatformTwapControlAction,
886    pub twap_id: String,
887    /// Backend-partially-signed transaction. The external session signer
888    /// verifies and fills only its signature slot.
889    pub transaction_base64: String,
890    pub recent_blockhash: String,
891    pub last_valid_block_height: u64,
892    pub expires_at_ms: u64,
893}
894
895#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
896#[serde(deny_unknown_fields)]
897pub struct PlatformTwapSubmitRequest {
898    pub twap_control_id: String,
899    pub signed_transaction_base64: String,
900    pub idempotency_key: String,
901}
902
903#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
904#[serde(deny_unknown_fields)]
905pub struct PlatformTwapSubmitResponse {
906    pub schema_version: u16,
907    pub contract_version: String,
908    pub twap_control_id: String,
909    pub market_id: String,
910    pub action: PlatformTwapControlAction,
911    pub twap_id: String,
912    pub signature: String,
913    pub status: PlatformOrderSubmissionStatus,
914}
915
916/// Sequenced wallet-scoped TWAP progress stream (`algos.twap.stream`) for one
917/// market. It starts with a `twaps_snapshot`, then sends one `twap_update`
918/// carrying the complete sanitized TWAP row whenever a schedule is created,
919/// executes a slice, or reaches a terminal state, plus heartbeats. Every event
920/// carries the stream identity and previous sequence; a recovery snapshot
921/// advances the sequence on the same identity.
922#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
923#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
924#[allow(clippy::large_enum_variant)]
925pub enum PlatformTwapEvent {
926    TwapsSnapshot {
927        schema_version: u16,
928        contract_version: String,
929        market_id: String,
930        wallet_address: String,
931        stream_id: String,
932        sequence: String,
933        server_time_ms: u64,
934        twaps: Vec<PlatformTwap>,
935    },
936    TwapUpdate {
937        schema_version: u16,
938        contract_version: String,
939        market_id: String,
940        wallet_address: String,
941        stream_id: String,
942        sequence: String,
943        previous_sequence: String,
944        server_time_ms: u64,
945        twap: PlatformTwap,
946    },
947    Heartbeat {
948        schema_version: u16,
949        contract_version: String,
950        market_id: String,
951        wallet_address: String,
952        stream_id: String,
953        sequence: String,
954        previous_sequence: String,
955        server_time_ms: u64,
956    },
957}
958
959#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
960pub enum PlatformPortfolioHistoryRange {
961    #[serde(rename = "24h")]
962    Day,
963    #[serde(rename = "7d")]
964    Week,
965    #[serde(rename = "30d")]
966    Month,
967}
968
969#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
970#[serde(deny_unknown_fields)]
971pub struct PlatformPortfolioHistoryPoint {
972    pub recorded_at_ms: u64,
973    pub equity_usd_micros: String,
974    pub available_usd_micros: String,
975    pub locked_usd_micros: String,
976    pub market_count: u32,
977}
978
979/// Stored account-equity history. It never fabricates data before collection
980/// began and keeps all currency values in exact USD micros.
981#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
982#[serde(deny_unknown_fields)]
983pub struct PlatformPortfolioHistoryResponse {
984    pub schema_version: u16,
985    pub contract_version: String,
986    pub wallet_address: String,
987    pub server_time_ms: u64,
988    pub range: PlatformPortfolioHistoryRange,
989    pub points: Vec<PlatformPortfolioHistoryPoint>,
990    pub collecting: bool,
991    pub first_sample_ms: Option<u64>,
992    pub last_sample_ms: Option<u64>,
993}
994
995/// One asset the owner holds on Strata, across every live market. Assets
996/// with no holdings are omitted. A balance is a balance: `total` is what the
997/// owner has, `available` is what is free to trade or withdraw, `locked` is
998/// what resting orders reserve.
999#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1000#[serde(deny_unknown_fields)]
1001pub struct PlatformPortfolioBalance {
1002    pub asset_id: String,
1003    /// Holdings not reserved by resting orders.
1004    pub available_atoms: String,
1005    /// Holdings reserved by resting orders.
1006    pub locked_atoms: String,
1007    /// `available_atoms + locked_atoms`.
1008    pub total_atoms: String,
1009    /// Exact USD micros for `total_atoms` when a fresh public mark exists.
1010    pub value_usd_micros: Option<String>,
1011}
1012
1013/// The owner's Vault position in one live market. Only markets where the
1014/// Vault holds a market account are listed.
1015#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1016#[serde(deny_unknown_fields)]
1017pub struct PlatformPortfolioPosition {
1018    pub market_id: String,
1019    pub base_asset_id: String,
1020    pub quote_asset_id: String,
1021    pub base_available_atoms: String,
1022    pub base_locked_atoms: String,
1023    pub quote_available_atoms: String,
1024    pub quote_locked_atoms: String,
1025}
1026
1027/// One open order, tagged with the market it rests in.
1028#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1029#[serde(deny_unknown_fields)]
1030pub struct PlatformPortfolioOrder {
1031    pub market_id: String,
1032    pub order_id: String,
1033    pub side: PlatformTradeSide,
1034    pub order_type: PlatformOrderType,
1035    pub state: PlatformOrderState,
1036    pub limit_price_atoms: String,
1037    pub original_size_atoms: String,
1038    pub remaining_size_atoms: String,
1039}
1040
1041/// One recent fill, tagged with the market it happened in.
1042#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1043#[serde(deny_unknown_fields)]
1044pub struct PlatformPortfolioFill {
1045    pub market_id: String,
1046    pub fill_id: String,
1047    pub side: PlatformTradeSide,
1048    pub price_atoms: String,
1049    pub size_atoms: String,
1050    pub fee_quote_atoms: String,
1051    pub fee_is_final: bool,
1052    pub settlement: PlatformSettlementState,
1053    pub executed_at_ms: u64,
1054    pub confirmed_at_ms: Option<u64>,
1055    pub transaction_id: Option<String>,
1056    pub realized_pnl_quote_atoms: String,
1057}
1058
1059/// The owner's whole account in one public read, by wallet address: balances,
1060/// per-market positions, open orders, and recent fills across every live
1061/// market, plus USD totals. No signature and no market selection is needed.
1062/// Amounts are exact atomic strings; USD totals are null whenever any held
1063/// asset lacks a fresh public mark, so a partial valuation is never presented
1064/// as complete.
1065#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1066#[serde(deny_unknown_fields)]
1067pub struct PlatformPortfolioResponse {
1068    pub schema_version: u16,
1069    pub contract_version: String,
1070    pub wallet_address: String,
1071    pub server_time_ms: u64,
1072    /// When the on-chain state behind this snapshot was observed.
1073    pub observed_at_ms: u64,
1074    /// Chain slot the snapshot was observed at.
1075    pub observed_slot: String,
1076    /// Live markets included in the snapshot.
1077    pub market_count: u32,
1078    pub balances: Vec<PlatformPortfolioBalance>,
1079    pub positions: Vec<PlatformPortfolioPosition>,
1080    /// Every open order across every live market.
1081    pub open_orders: Vec<PlatformPortfolioOrder>,
1082    /// Recent fills across every live market, newest first (bounded).
1083    pub recent_fills: Vec<PlatformPortfolioFill>,
1084    /// Markets whose orders and fills could not be read for this snapshot;
1085    /// balances and positions are still complete.
1086    pub unavailable_market_ids: Vec<String>,
1087    /// Sum of every balance's `value_usd_micros`; null unless the valuation is complete.
1088    pub equity_usd_micros: Option<String>,
1089    /// Exact USD value of every available balance; null unless the valuation is complete.
1090    pub available_usd_micros: Option<String>,
1091    /// `equity_usd_micros - available_usd_micros`; null unless the valuation is complete.
1092    pub locked_usd_micros: Option<String>,
1093    pub valuation_complete: bool,
1094    pub unpriced_asset_ids: Vec<String>,
1095}
1096
1097#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1098#[serde(rename_all = "snake_case")]
1099pub enum PlatformVaultState {
1100    Absent,
1101    Active,
1102    Paused,
1103}
1104
1105#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1106#[serde(rename_all = "snake_case")]
1107pub enum PlatformVaultSessionState {
1108    Absent,
1109    Active,
1110    Expired,
1111}
1112
1113#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1114#[serde(rename_all = "snake_case")]
1115pub enum PlatformVaultWithdrawalMode {
1116    Unrestricted,
1117    Blocked,
1118    Restricted,
1119}
1120
1121/// One asset-specific execution limit. A null maximum means that the session
1122/// is permitted to use the asset without a per-execution amount ceiling.
1123#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1124#[serde(deny_unknown_fields)]
1125pub struct PlatformVaultSpendingLimit {
1126    pub asset_id: String,
1127    pub maximum_per_execution_atoms: Option<String>,
1128}
1129
1130/// Sanitized state for the requested external session key. It intentionally
1131/// omits all construction accounts and price-source identities.
1132#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1133#[serde(deny_unknown_fields)]
1134pub struct PlatformVaultSessionStatus {
1135    pub session_public_key: String,
1136    pub state: PlatformVaultSessionState,
1137    pub expires_at_ms: Option<u64>,
1138    pub permanent: bool,
1139    pub minimum_interval_seconds: u32,
1140    pub maximum_tolerance_bps: u16,
1141    pub last_execution_at_ms: Option<u64>,
1142    pub market_execution_ready: bool,
1143    pub price_protection_active: bool,
1144    pub spending_limits: Vec<PlatformVaultSpendingLimit>,
1145}
1146
1147#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1148#[serde(deny_unknown_fields)]
1149pub struct PlatformVaultWithdrawalAccess {
1150    pub mode: PlatformVaultWithdrawalMode,
1151    pub allowed_wallet_addresses: Vec<String>,
1152}
1153
1154/// Product-level Vault state for an owner and, when requested, one external
1155/// session key. Chain construction identities never cross this boundary.
1156#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1157#[serde(deny_unknown_fields)]
1158pub struct PlatformVaultStatusResponse {
1159    pub schema_version: u16,
1160    pub contract_version: String,
1161    pub server_time_ms: u64,
1162    pub wallet_address: String,
1163    pub state: PlatformVaultState,
1164    pub session: Option<PlatformVaultSessionStatus>,
1165    pub withdrawal_access: PlatformVaultWithdrawalAccess,
1166}
1167
1168#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1169#[serde(deny_unknown_fields)]
1170pub struct PlatformVaultPausePrepareRequest {
1171    pub wallet_address: String,
1172    pub paused: bool,
1173}
1174
1175/// An unsigned owner transaction. The external owner must verify its wallet
1176/// and requested state before signing and broadcasting it.
1177#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1178#[serde(deny_unknown_fields)]
1179pub struct PlatformVaultPausePrepareResponse {
1180    pub schema_version: u16,
1181    pub contract_version: String,
1182    pub server_time_ms: u64,
1183    pub wallet_address: String,
1184    pub paused: bool,
1185    pub transaction_base64: String,
1186    pub recent_blockhash: String,
1187    pub owner_signature_required: bool,
1188    /// Opaque handle for this prepared transaction. Hand it back with the
1189    /// owner-signed transaction to `vault.relay` and Strata submits it — no
1190    /// RPC or SOL needed on the owner side.
1191    pub preparation_id: String,
1192    /// `true` when Strata is the transaction fee payer and covers any rent the
1193    /// action creates, so the owner needs no SOL at all. `false` means the
1194    /// owner wallet is the fee payer (Strata still submits it on request).
1195    pub sponsored: bool,
1196    /// The prepared transaction must be submitted before this server time.
1197    pub submit_by_ms: u64,
1198}
1199
1200#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1201#[serde(rename_all = "snake_case")]
1202pub enum PlatformVaultSetupMode {
1203    Create,
1204    ReplaceSession,
1205}
1206
1207/// Session policy applied when onboarding does not state one: at most one
1208/// execution per second per session, and a 1% maximum tolerance.
1209pub const PLATFORM_SESSION_DEFAULT_MINIMUM_INTERVAL_SECONDS: u32 = 1;
1210pub const PLATFORM_SESSION_DEFAULT_MAXIMUM_TOLERANCE_BPS: u16 = 100;
1211/// A session carries at most this many spending limits.
1212pub const PLATFORM_SESSION_MAX_SPENDING_LIMITS: usize = 4;
1213
1214/// One-signature onboarding: only the wallet and the external session key are
1215/// required. One session then trades every market. Everything else is an
1216/// optional policy on top; absent values take the product defaults.
1217#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1218#[serde(deny_unknown_fields)]
1219pub struct PlatformVaultSetupPrepareRequest {
1220    pub wallet_address: String,
1221    pub session_public_key: String,
1222    /// Optional old session key to revoke in the same transaction that
1223    /// registers `session_public_key`. If it is already absent, setup still
1224    /// succeeds. This makes local credential rotation one owner signature.
1225    #[serde(default)]
1226    pub replace_session_public_key: Option<String>,
1227    /// Optional. Names the market whose price protection the session pins
1228    /// when the product has one; the session trades every market either way.
1229    #[serde(default)]
1230    pub market_id: Option<String>,
1231    /// Null or absent requests the permanent-session expiry supported by the
1232    /// product.
1233    #[serde(default)]
1234    pub expires_at_ms: Option<u64>,
1235    /// Absent takes `PLATFORM_SESSION_DEFAULT_MINIMUM_INTERVAL_SECONDS`.
1236    #[serde(default)]
1237    pub minimum_interval_seconds: Option<u32>,
1238    /// Absent takes `PLATFORM_SESSION_DEFAULT_MAXIMUM_TOLERANCE_BPS`.
1239    #[serde(default)]
1240    pub maximum_tolerance_bps: Option<u16>,
1241    /// Optional per-asset ceilings, at most `PLATFORM_SESSION_MAX_SPENDING_LIMITS`.
1242    /// Assets without a limit are unlimited.
1243    #[serde(default)]
1244    pub spending_limits: Vec<PlatformVaultSpendingLimit>,
1245}
1246
1247/// Owner-bound onboarding or session-replacement transaction. Product inputs
1248/// are echoed exactly so an external verifier can reject changed intent.
1249#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1250#[serde(deny_unknown_fields)]
1251pub struct PlatformVaultSetupPrepareResponse {
1252    pub schema_version: u16,
1253    pub contract_version: String,
1254    pub server_time_ms: u64,
1255    pub wallet_address: String,
1256    pub session_public_key: String,
1257    /// The old session requested for atomic replacement, if any.
1258    pub replace_session_public_key: Option<String>,
1259    /// The market named in the request, if any.
1260    pub market_id: Option<String>,
1261    pub mode: PlatformVaultSetupMode,
1262    pub expires_at_ms: Option<u64>,
1263    pub permanent: bool,
1264    /// The applied policy, defaults resolved.
1265    pub minimum_interval_seconds: u32,
1266    pub maximum_tolerance_bps: u16,
1267    pub spending_limits: Vec<PlatformVaultSpendingLimit>,
1268    pub transaction_base64: String,
1269    pub recent_blockhash: String,
1270    pub owner_signature_required: bool,
1271    /// Opaque handle for this prepared transaction. Hand it back with the
1272    /// owner-signed transaction to `vault.relay` and Strata submits it — no
1273    /// RPC or SOL needed on the owner side.
1274    pub preparation_id: String,
1275    /// `true` when Strata is the transaction fee payer and covers any rent the
1276    /// action creates, so the owner needs no SOL at all. `false` means the
1277    /// owner wallet is the fee payer (Strata still submits it on request).
1278    pub sponsored: bool,
1279    /// The prepared transaction must be submitted before this server time.
1280    pub submit_by_ms: u64,
1281}
1282
1283#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1284#[serde(rename_all = "snake_case")]
1285pub enum PlatformVaultDelegateAction {
1286    Revoke,
1287}
1288
1289#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1290#[serde(deny_unknown_fields)]
1291pub struct PlatformVaultDelegatePrepareRequest {
1292    pub wallet_address: String,
1293    pub session_public_key: String,
1294    pub action: PlatformVaultDelegateAction,
1295}
1296
1297/// Unsigned session-lifecycle control. The owner verifies both identities and
1298/// the destructive action before signing and broadcasting it externally.
1299#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1300#[serde(deny_unknown_fields)]
1301pub struct PlatformVaultDelegatePrepareResponse {
1302    pub schema_version: u16,
1303    pub contract_version: String,
1304    pub server_time_ms: u64,
1305    pub wallet_address: String,
1306    pub session_public_key: String,
1307    pub action: PlatformVaultDelegateAction,
1308    pub transaction_base64: String,
1309    pub recent_blockhash: String,
1310    pub owner_signature_required: bool,
1311    /// Opaque handle for this prepared transaction. Hand it back with the
1312    /// owner-signed transaction to `vault.relay` and Strata submits it — no
1313    /// RPC or SOL needed on the owner side.
1314    pub preparation_id: String,
1315    /// `true` when Strata is the transaction fee payer and covers any rent the
1316    /// action creates, so the owner needs no SOL at all. `false` means the
1317    /// owner wallet is the fee payer (Strata still submits it on request).
1318    pub sponsored: bool,
1319    /// The prepared transaction must be submitted before this server time.
1320    pub submit_by_ms: u64,
1321}
1322
1323#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1324#[serde(deny_unknown_fields)]
1325pub struct PlatformVaultPolicyPrepareRequest {
1326    pub wallet_address: String,
1327    pub withdrawal_access: PlatformVaultWithdrawalAccess,
1328}
1329
1330/// An owner-bound withdrawal-access change. Unrestricted access is a status
1331/// state rather than a preparable action in this contract.
1332#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1333#[serde(deny_unknown_fields)]
1334pub struct PlatformVaultPolicyPrepareResponse {
1335    pub schema_version: u16,
1336    pub contract_version: String,
1337    pub server_time_ms: u64,
1338    pub wallet_address: String,
1339    pub withdrawal_access: PlatformVaultWithdrawalAccess,
1340    pub transaction_base64: String,
1341    pub recent_blockhash: String,
1342    pub owner_signature_required: bool,
1343    /// Opaque handle for this prepared transaction. Hand it back with the
1344    /// owner-signed transaction to `vault.relay` and Strata submits it — no
1345    /// RPC or SOL needed on the owner side.
1346    pub preparation_id: String,
1347    /// `true` when Strata is the transaction fee payer and covers any rent the
1348    /// action creates, so the owner needs no SOL at all. `false` means the
1349    /// owner wallet is the fee payer (Strata still submits it on request).
1350    pub sponsored: bool,
1351    /// The prepared transaction must be submitted before this server time.
1352    pub submit_by_ms: u64,
1353}
1354
1355#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1356#[serde(deny_unknown_fields)]
1357pub struct PlatformVaultDepositPrepareRequest {
1358    pub wallet_address: String,
1359    pub market_id: String,
1360    pub asset_id: String,
1361    pub amount_atoms: String,
1362    /// Optional external session key. When it is not yet registered for this
1363    /// wallet, the same deposit transaction registers it with the default
1364    /// session policy — a first deposit is the whole onboarding, one owner
1365    /// signature. An already-registered key changes nothing.
1366    #[serde(default)]
1367    pub session_public_key: Option<String>,
1368}
1369
1370/// Exact owner-funded deposit transaction. Asset construction and custody
1371/// identities remain internal; the public intent is echoed for verification.
1372#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1373#[serde(deny_unknown_fields)]
1374pub struct PlatformVaultDepositPrepareResponse {
1375    pub schema_version: u16,
1376    pub contract_version: String,
1377    pub server_time_ms: u64,
1378    pub wallet_address: String,
1379    pub market_id: String,
1380    pub asset_id: String,
1381    pub amount_atoms: String,
1382    /// SOL Strata already spent on this owner's sponsored actions, recovered
1383    /// in the deposit asset inside this same transaction (a second transfer
1384    /// from the owner's account to Strata). "0" when nothing is owed. It is
1385    /// only ever charged when the owner had no SOL and Strata paid instead,
1386    /// and never exceeds 1% of the deposit.
1387    pub network_cost_atoms: String,
1388    /// The session key named in the request, if any.
1389    pub session_public_key: Option<String>,
1390    /// `true` when this transaction also registers `session_public_key` with
1391    /// the default session policy (the deposit doubles as onboarding);
1392    /// `false` when the key was already registered or none was named.
1393    pub registers_session: bool,
1394    pub transaction_base64: String,
1395    pub recent_blockhash: String,
1396    pub owner_signature_required: bool,
1397    /// Opaque handle for this prepared transaction. Hand it back with the
1398    /// owner-signed transaction to `vault.relay` and Strata submits it — no
1399    /// RPC or SOL needed on the owner side.
1400    pub preparation_id: String,
1401    /// `true` when Strata is the transaction fee payer and covers any rent the
1402    /// action creates, so the owner needs no SOL at all. `false` means the
1403    /// owner wallet is the fee payer (Strata still submits it on request).
1404    pub sponsored: bool,
1405    /// The prepared transaction must be submitted before this server time.
1406    pub submit_by_ms: u64,
1407}
1408
1409#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1410#[serde(deny_unknown_fields)]
1411pub struct PlatformVaultWithdrawPrepareRequest {
1412    pub wallet_address: String,
1413    pub market_id: String,
1414    pub asset_id: String,
1415    pub destination_wallet_address: String,
1416    pub amount_atoms: String,
1417}
1418
1419/// Exact owner-authorized withdrawal transaction. The destination is a wallet
1420/// identity; account construction and private balance routing remain internal.
1421#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1422#[serde(deny_unknown_fields)]
1423pub struct PlatformVaultWithdrawPrepareResponse {
1424    pub schema_version: u16,
1425    pub contract_version: String,
1426    pub server_time_ms: u64,
1427    pub wallet_address: String,
1428    pub market_id: String,
1429    pub asset_id: String,
1430    pub destination_wallet_address: String,
1431    pub amount_atoms: String,
1432    pub transaction_base64: String,
1433    pub recent_blockhash: String,
1434    pub owner_signature_required: bool,
1435    /// Opaque handle for this prepared transaction. Hand it back with the
1436    /// owner-signed transaction to `vault.relay` and Strata submits it — no
1437    /// RPC or SOL needed on the owner side.
1438    pub preparation_id: String,
1439    /// `true` when Strata is the transaction fee payer and covers any rent the
1440    /// action creates, so the owner needs no SOL at all. `false` means the
1441    /// owner wallet is the fee payer (Strata still submits it on request).
1442    pub sponsored: bool,
1443    /// The prepared transaction must be submitted before this server time.
1444    pub submit_by_ms: u64,
1445}
1446
1447/// Which prepared Vault action a submission carries.
1448#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1449#[serde(rename_all = "snake_case")]
1450pub enum PlatformVaultAction {
1451    Setup,
1452    Deposit,
1453    Withdraw,
1454    Delegate,
1455    Policy,
1456    Pause,
1457}
1458
1459#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1460#[serde(rename_all = "snake_case")]
1461pub enum PlatformVaultSubmissionStatus {
1462    /// Accepted by the cluster; confirmation pending.
1463    Submitted,
1464    Confirmed,
1465    Failed,
1466}
1467
1468/// Submit an owner-signed prepared Vault transaction. Strata verifies it is
1469/// exactly the prepared transaction, adds its own fee-payer signature when
1470/// the preparation was sponsored, and broadcasts it. Idempotent per
1471/// `idempotency_key`.
1472#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1473#[serde(deny_unknown_fields)]
1474pub struct PlatformVaultSubmitRequest {
1475    pub preparation_id: String,
1476    pub signed_transaction_base64: String,
1477    pub idempotency_key: String,
1478}
1479
1480/// Durable outcome of a Vault submission, also returned by the status read.
1481#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1482#[serde(deny_unknown_fields)]
1483pub struct PlatformVaultSubmitResponse {
1484    pub schema_version: u16,
1485    pub contract_version: String,
1486    pub preparation_id: String,
1487    pub action: PlatformVaultAction,
1488    pub wallet_address: String,
1489    pub sponsored: bool,
1490    pub signature: String,
1491    pub status: PlatformVaultSubmissionStatus,
1492    /// Present only when `status` is `failed`.
1493    pub failure_code: Option<String>,
1494    pub updated_at_ms: u64,
1495}
1496
1497#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1498#[serde(deny_unknown_fields)]
1499pub struct PlatformRewardStanding {
1500    pub rank: u32,
1501    pub wallet_address: String,
1502    pub points: String,
1503}
1504
1505#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1506#[serde(deny_unknown_fields)]
1507pub struct PlatformOwnerRewards {
1508    pub wallet_address: String,
1509    pub rank: Option<u32>,
1510    pub points: String,
1511    pub trading_points: String,
1512    pub making_points: String,
1513    pub bug_points: String,
1514    pub referral_points: String,
1515}
1516
1517#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1518#[serde(deny_unknown_fields)]
1519pub struct PlatformRewardsResponse {
1520    pub schema_version: u16,
1521    pub contract_version: String,
1522    pub server_time_ms: u64,
1523    pub season: String,
1524    pub total_wallets: u32,
1525    pub owner: Option<PlatformOwnerRewards>,
1526    pub standings: Vec<PlatformRewardStanding>,
1527}
1528
1529#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1530#[serde(deny_unknown_fields)]
1531pub struct PlatformReferralsResponse {
1532    pub schema_version: u16,
1533    pub contract_version: String,
1534    pub server_time_ms: u64,
1535    pub wallet_address: String,
1536    pub enabled: bool,
1537    pub cash_rewards_enabled: bool,
1538    pub referral_code: Option<String>,
1539    pub referred_wallets: u32,
1540    pub referral_points: String,
1541    pub referred_by: Option<String>,
1542    pub referral_locked: bool,
1543    pub cash_accrued_atoms: String,
1544    pub cash_paid_atoms: String,
1545    pub cash_claimable_atoms: String,
1546}
1547
1548#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1549#[serde(deny_unknown_fields)]
1550pub struct PlatformReferralLinkRequest {
1551    pub wallet_address: String,
1552    pub referral_code: String,
1553    pub authorization_signature: String,
1554}
1555
1556#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1557#[serde(deny_unknown_fields)]
1558pub struct PlatformReferralLinkResponse {
1559    pub schema_version: u16,
1560    pub contract_version: String,
1561    pub server_time_ms: u64,
1562    pub wallet_address: String,
1563    pub referral_code: String,
1564    pub status: String,
1565}
1566
1567#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1568#[serde(deny_unknown_fields)]
1569pub struct PlatformReferralClaimRequest {
1570    pub wallet_address: String,
1571    pub payout_wallet_address: Option<String>,
1572    pub authorization_signature: String,
1573}
1574
1575#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1576#[serde(deny_unknown_fields)]
1577pub struct PlatformReferralClaimResponse {
1578    pub schema_version: u16,
1579    pub contract_version: String,
1580    pub server_time_ms: u64,
1581    pub wallet_address: String,
1582    pub payout_wallet_address: String,
1583    pub claimable_atoms: String,
1584    pub status: String,
1585}
1586
1587#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1588#[serde(rename_all = "snake_case")]
1589pub enum PlatformBugStatus {
1590    Pending,
1591    Confirmed,
1592    Rejected,
1593}
1594
1595#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1596#[serde(deny_unknown_fields)]
1597pub struct PlatformBugReport {
1598    pub bug_id: String,
1599    pub status: PlatformBugStatus,
1600    pub severity: u8,
1601    pub points: String,
1602    pub created_at_ms: u64,
1603    pub triaged_at_ms: Option<u64>,
1604    pub completed_at_ms: Option<u64>,
1605}
1606
1607#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1608#[serde(deny_unknown_fields)]
1609pub struct PlatformBugsResponse {
1610    pub schema_version: u16,
1611    pub contract_version: String,
1612    pub server_time_ms: u64,
1613    pub wallet_address: String,
1614    pub points: String,
1615    pub confirmed_reports: u32,
1616    pub reports: Vec<PlatformBugReport>,
1617}
1618
1619#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1620#[serde(deny_unknown_fields)]
1621pub struct PlatformBugSubmitRequest {
1622    pub owner_wallet: String,
1623    pub message: String,
1624    /// Hex Ed25519 signature over `strata-bug-report:v1:` followed by the
1625    /// trimmed report message. Signing always happens outside Strata.
1626    pub authorization_signature: String,
1627}
1628
1629#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1630#[serde(deny_unknown_fields)]
1631pub struct PlatformBugSubmitResponse {
1632    pub schema_version: u16,
1633    pub contract_version: String,
1634    pub server_time_ms: u64,
1635    pub bug_id: String,
1636    pub status: PlatformBugStatus,
1637}
1638
1639#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1640#[serde(rename_all = "snake_case")]
1641pub enum PlatformTradeSide {
1642    Buy,
1643    Sell,
1644}
1645
1646#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1647#[serde(deny_unknown_fields)]
1648pub struct PlatformTrade {
1649    pub trade_id: String,
1650    pub side: PlatformTradeSide,
1651    pub price_atoms: String,
1652    pub size_atoms: String,
1653    pub executed_at_ms: u64,
1654}
1655
1656#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1657#[serde(deny_unknown_fields)]
1658pub struct PlatformTradesResponse {
1659    pub schema_version: u16,
1660    pub contract_version: String,
1661    pub market_id: String,
1662    pub server_time_ms: u64,
1663    pub trades: Vec<PlatformTrade>,
1664}
1665
1666#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1667#[serde(rename_all = "snake_case")]
1668pub enum PlatformOrderType {
1669    GoodUntilCancelled,
1670    ImmediateOrCancel,
1671    FillOrKill,
1672    PostOnly,
1673}
1674
1675/// Externally authorized resting-order operation. The public contract exposes
1676/// product intent only; private construction details never cross the SDK
1677/// boundary.
1678#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1679#[serde(rename_all = "snake_case")]
1680pub enum PlatformOrderAction {
1681    Place,
1682    Cancel,
1683    CancelAll,
1684    /// Atomically cancel one existing order and place its explicitly bound
1685    /// successor in the same transaction.
1686    Replace,
1687    /// Atomically execute a bounded heterogeneous set of place, cancel, and
1688    /// replace operations in one transaction.
1689    Batch,
1690}
1691
1692/// One operation inside an atomic order-control batch. Owner and session
1693/// identity live on the enclosing challenge so no item can widen authority.
1694#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1695#[serde(tag = "action", rename_all = "snake_case", deny_unknown_fields)]
1696pub enum PlatformOrderBatchOperation {
1697    Place {
1698        /// Vault market account sequence for this order. Omit it and Strata
1699        /// resolves the next sequence from the Vault's confirmed market
1700        /// account when the challenge is issued (consecutive places in one
1701        /// batch receive consecutive sequences); supply it to pin a sequence
1702        /// tracked locally. A batch must either supply every sequence or none.
1703        #[serde(default, skip_serializing_if = "Option::is_none")]
1704        account_sequence: Option<String>,
1705        client_order_id: String,
1706        side: PlatformTradeSide,
1707        order_type: PlatformOrderType,
1708        limit_price_atoms: String,
1709        size_atoms: String,
1710    },
1711    Cancel {
1712        order_id: String,
1713    },
1714    Replace {
1715        order_id: String,
1716        #[serde(default, skip_serializing_if = "Option::is_none")]
1717        account_sequence: Option<String>,
1718        client_order_id: String,
1719        side: PlatformTradeSide,
1720        order_type: PlatformOrderType,
1721        limit_price_atoms: String,
1722        size_atoms: String,
1723    },
1724}
1725
1726/// Request canonical bytes for one externally signed order-control operation.
1727/// Variant-specific fields are sealed so an authorization cannot be widened
1728/// between challenge and transaction preparation.
1729#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1730#[serde(tag = "action", rename_all = "snake_case", deny_unknown_fields)]
1731pub enum PlatformOrderChallengeRequest {
1732    Place {
1733        owner_wallet: String,
1734        session_public_key: String,
1735        /// Vault market account sequence. Omit it and Strata resolves the next
1736        /// sequence from the Vault's confirmed market account when the
1737        /// challenge is issued; supply it to pin a sequence tracked locally.
1738        #[serde(default, skip_serializing_if = "Option::is_none")]
1739        account_sequence: Option<String>,
1740        client_order_id: String,
1741        side: PlatformTradeSide,
1742        order_type: PlatformOrderType,
1743        limit_price_atoms: String,
1744        size_atoms: String,
1745    },
1746    Cancel {
1747        owner_wallet: String,
1748        session_public_key: String,
1749        order_id: String,
1750    },
1751    CancelAll {
1752        owner_wallet: String,
1753        session_public_key: String,
1754    },
1755    Replace {
1756        owner_wallet: String,
1757        session_public_key: String,
1758        order_id: String,
1759        #[serde(default, skip_serializing_if = "Option::is_none")]
1760        account_sequence: Option<String>,
1761        client_order_id: String,
1762        side: PlatformTradeSide,
1763        order_type: PlatformOrderType,
1764        limit_price_atoms: String,
1765        size_atoms: String,
1766    },
1767    Batch {
1768        owner_wallet: String,
1769        session_public_key: String,
1770        operations: Vec<PlatformOrderBatchOperation>,
1771    },
1772}
1773
1774#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1775#[serde(deny_unknown_fields)]
1776pub struct PlatformOrderChallengeResponse {
1777    pub schema_version: u16,
1778    pub contract_version: String,
1779    pub challenge_id: String,
1780    pub market_id: String,
1781    pub action: PlatformOrderAction,
1782    /// Exact opaque order set bound by the authorization. Replace returns the
1783    /// old then new ID. Batch flattens item IDs in request order, with replace
1784    /// contributing old then new. A batch contains at most six operations.
1785    pub order_ids: Vec<String>,
1786    pub authorization_payload_base64: String,
1787    pub server_time_ms: u64,
1788    pub expires_at_ms: u64,
1789}
1790
1791/// A prepared challenge, signed: the two-step path.
1792#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1793#[serde(deny_unknown_fields)]
1794pub struct PlatformOrderPrepareAuthorization {
1795    pub challenge_id: String,
1796    /// Base58 Ed25519 signature over `authorization_payload_base64`. Required
1797    /// over HTTP. Over the session-authenticated order command channel it may
1798    /// be omitted: the socket already proved the session and the challenge is
1799    /// bound to it, so the session signs only the transaction (one signature).
1800    #[serde(default, skip_serializing_if = "Option::is_none")]
1801    pub authorization_signature: Option<String>,
1802}
1803
1804/// Prepare an order-control transaction. Either hand back a signed challenge
1805/// (`Authorized`, two signatures per action) or send the operation itself
1806/// (`Direct`, one signature per action): Strata builds the transaction from
1807/// the operation immediately and the session's signature over that
1808/// transaction is the whole authorization. The response is identical.
1809#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1810#[serde(untagged)]
1811pub enum PlatformOrderPrepareRequest {
1812    Authorized(PlatformOrderPrepareAuthorization),
1813    Direct(PlatformOrderChallengeRequest),
1814}
1815
1816#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1817#[serde(deny_unknown_fields)]
1818pub struct PlatformOrderPrepareResponse {
1819    pub schema_version: u16,
1820    pub contract_version: String,
1821    pub order_control_id: String,
1822    pub market_id: String,
1823    pub action: PlatformOrderAction,
1824    pub order_ids: Vec<String>,
1825    /// Backend-partially-signed Solana v0 transaction. The external session
1826    /// signer verifies and fills only its signature slot.
1827    pub transaction_base64: String,
1828    pub recent_blockhash: String,
1829    pub last_valid_block_height: u64,
1830    pub expires_at_ms: u64,
1831}
1832
1833#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1834#[serde(deny_unknown_fields)]
1835pub struct PlatformOrderSubmitRequest {
1836    pub order_control_id: String,
1837    pub signed_transaction_base64: String,
1838    pub idempotency_key: String,
1839}
1840
1841#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1842#[serde(rename_all = "snake_case")]
1843pub enum PlatformOrderSubmissionStatus {
1844    Submitted,
1845}
1846
1847#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1848#[serde(deny_unknown_fields)]
1849pub struct PlatformOrderSubmitResponse {
1850    pub schema_version: u16,
1851    pub contract_version: String,
1852    pub order_control_id: String,
1853    pub market_id: String,
1854    pub action: PlatformOrderAction,
1855    pub order_ids: Vec<String>,
1856    pub signature: String,
1857    pub status: PlatformOrderSubmissionStatus,
1858}
1859
1860#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1861#[serde(deny_unknown_fields)]
1862pub struct PlatformOrderStatusRequest {
1863    pub order_control_id: String,
1864    pub idempotency_key: String,
1865}
1866
1867#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1868#[serde(rename_all = "snake_case")]
1869pub enum PlatformOrderControlStatus {
1870    Submitting,
1871    Submitted,
1872    Failed,
1873}
1874
1875#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1876#[serde(deny_unknown_fields)]
1877pub struct PlatformOrderStatusResponse {
1878    pub schema_version: u16,
1879    pub contract_version: String,
1880    pub order_control_id: String,
1881    pub market_id: String,
1882    pub action: PlatformOrderAction,
1883    pub order_ids: Vec<String>,
1884    pub signature: String,
1885    pub status: PlatformOrderControlStatus,
1886    pub failure_code: Option<String>,
1887    pub updated_at_ms: u64,
1888}
1889
1890/// Collision policy for an incoming order that would cross the owner's own
1891/// resting liquidity. Every mode still preserves Strata's matcher and on-chain
1892/// self-fill prohibition; this only controls which order is cancelled first.
1893#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1894#[serde(rename_all = "snake_case")]
1895pub enum PlatformSelfTradePrevention {
1896    CancelTaker,
1897    CancelMaker,
1898    CancelBoth,
1899    SkipOwnLiquidity,
1900}
1901
1902/// One command on the persistent order-control connection. Challenge results
1903/// may contain an effective request that differs from the requested one only
1904/// by the explicitly selected self-trade prevention transformation.
1905#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1906#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
1907pub enum PlatformOrderCommand {
1908    /// Authenticated non-trading round trip used for latency certification.
1909    Probe {
1910        nonce: String,
1911    },
1912    Challenge {
1913        request: PlatformOrderChallengeRequest,
1914        self_trade_prevention: PlatformSelfTradePrevention,
1915    },
1916    Prepare {
1917        request: PlatformOrderPrepareRequest,
1918    },
1919    Submit {
1920        request: PlatformOrderSubmitRequest,
1921    },
1922    Status {
1923        request: PlatformOrderStatusRequest,
1924    },
1925    DeadManArm {
1926        timeout_ms: u64,
1927        request: PlatformOrderSubmitRequest,
1928    },
1929    DeadManStatus,
1930    DeadManHeartbeat,
1931    DeadManDisarm,
1932}
1933
1934/// Frames sent by an external agent. Authentication proves possession of the
1935/// declared session key; individual order authorizations and transactions keep
1936/// their existing exact external-signing boundaries. Authentication is a
1937/// singleton frame. After authentication, the transport accepts either one
1938/// command or a bounded array of commands; every command retains its own
1939/// request ID and contiguous sequence.
1940#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1941#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
1942pub enum PlatformOrderCommandClientFrame {
1943    Authenticate {
1944        owner_wallet: String,
1945        session_public_key: String,
1946        /// Base58 Ed25519 signature over the stream authentication payload.
1947        signature: String,
1948        /// Optional negotiated result framing. Omitted clients retain the
1949        /// complete-event array format.
1950        #[serde(default, skip_serializing_if = "Option::is_none")]
1951        batch_format: Option<PlatformOrderCommandBatchFormat>,
1952    },
1953    Command {
1954        request_id: String,
1955        sequence: String,
1956        command: PlatformOrderCommand,
1957    },
1958}
1959
1960#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1961#[serde(rename_all = "snake_case")]
1962pub enum PlatformOrderCommandBatchFormat {
1963    CompactV1,
1964}
1965
1966/// One result inside a compact event batch. Shared stream identity, time and
1967/// sequence metadata live on the enclosing frame; request correlation and
1968/// command-specific results remain independent.
1969#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1970#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
1971pub enum PlatformOrderCommandBatchEvent {
1972    ProbeResult {
1973        request_id: String,
1974        nonce: String,
1975    },
1976    ChallengeResult {
1977        request_id: String,
1978        self_trade_prevention: PlatformSelfTradePrevention,
1979        prevented_order_ids: Vec<String>,
1980        effective_request: PlatformOrderChallengeRequest,
1981        response: PlatformOrderChallengeResponse,
1982    },
1983    PrepareResult {
1984        request_id: String,
1985        response: PlatformOrderPrepareResponse,
1986    },
1987    SubmitResult {
1988        request_id: String,
1989        response: PlatformOrderSubmitResponse,
1990    },
1991    StatusResult {
1992        request_id: String,
1993        response: PlatformOrderStatusResponse,
1994    },
1995    DeadManResult {
1996        request_id: String,
1997        state: PlatformDeadManState,
1998    },
1999    CommandError {
2000        request_id: String,
2001        error: PublicOperationError,
2002    },
2003    Heartbeat,
2004}
2005
2006#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2007#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
2008pub enum PlatformOrderCommandServerFrame {
2009    EventBatch {
2010        schema_version: u16,
2011        contract_version: String,
2012        market_id: String,
2013        stream_id: String,
2014        first_sequence: String,
2015        previous_sequence: String,
2016        server_time_ms: u64,
2017        events: Vec<PlatformOrderCommandBatchEvent>,
2018    },
2019}
2020
2021#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
2022#[serde(rename_all = "snake_case")]
2023pub enum PlatformDeadManStatus {
2024    Armed,
2025    Triggering,
2026    Triggered,
2027    Disarmed,
2028    Expired,
2029    Failed,
2030}
2031
2032#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2033#[serde(deny_unknown_fields)]
2034pub struct PlatformDeadManState {
2035    pub status: PlatformDeadManStatus,
2036    pub timeout_ms: u64,
2037    pub heartbeat_deadline_ms: u64,
2038    pub order_control_id: Option<String>,
2039    pub signature: Option<String>,
2040    pub failure_code: Option<String>,
2041    pub updated_at_ms: u64,
2042}
2043
2044/// One sequenced event emitted by the persistent order-control connection.
2045/// After authentication, the transport carries bounded arrays of these events
2046/// so concurrent results share frame overhead without weakening per-event
2047/// sequence or request correlation. Terminal chain status may arrive later
2048/// without blocking command submission.
2049#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2050#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
2051pub enum PlatformOrderCommandEvent {
2052    AuthChallenge {
2053        schema_version: u16,
2054        contract_version: String,
2055        market_id: String,
2056        challenge: String,
2057        server_time_ms: u64,
2058        expires_at_ms: u64,
2059    },
2060    Ready {
2061        schema_version: u16,
2062        contract_version: String,
2063        market_id: String,
2064        stream_id: String,
2065        sequence: String,
2066        server_time_ms: u64,
2067    },
2068    ProbeResult {
2069        schema_version: u16,
2070        contract_version: String,
2071        market_id: String,
2072        stream_id: String,
2073        sequence: String,
2074        previous_sequence: String,
2075        request_id: String,
2076        nonce: String,
2077        server_time_ms: u64,
2078    },
2079    ChallengeResult {
2080        schema_version: u16,
2081        contract_version: String,
2082        market_id: String,
2083        stream_id: String,
2084        sequence: String,
2085        previous_sequence: String,
2086        request_id: String,
2087        self_trade_prevention: PlatformSelfTradePrevention,
2088        prevented_order_ids: Vec<String>,
2089        effective_request: PlatformOrderChallengeRequest,
2090        response: PlatformOrderChallengeResponse,
2091        server_time_ms: u64,
2092    },
2093    PrepareResult {
2094        schema_version: u16,
2095        contract_version: String,
2096        market_id: String,
2097        stream_id: String,
2098        sequence: String,
2099        previous_sequence: String,
2100        request_id: String,
2101        response: PlatformOrderPrepareResponse,
2102        server_time_ms: u64,
2103    },
2104    SubmitResult {
2105        schema_version: u16,
2106        contract_version: String,
2107        market_id: String,
2108        stream_id: String,
2109        sequence: String,
2110        previous_sequence: String,
2111        request_id: String,
2112        response: PlatformOrderSubmitResponse,
2113        server_time_ms: u64,
2114    },
2115    StatusResult {
2116        schema_version: u16,
2117        contract_version: String,
2118        market_id: String,
2119        stream_id: String,
2120        sequence: String,
2121        previous_sequence: String,
2122        request_id: String,
2123        response: PlatformOrderStatusResponse,
2124        server_time_ms: u64,
2125    },
2126    DeadManResult {
2127        schema_version: u16,
2128        contract_version: String,
2129        market_id: String,
2130        stream_id: String,
2131        sequence: String,
2132        previous_sequence: String,
2133        request_id: String,
2134        state: PlatformDeadManState,
2135        server_time_ms: u64,
2136    },
2137    CommandError {
2138        schema_version: u16,
2139        contract_version: String,
2140        market_id: String,
2141        stream_id: String,
2142        sequence: String,
2143        previous_sequence: String,
2144        request_id: String,
2145        error: PublicOperationError,
2146        server_time_ms: u64,
2147    },
2148    Heartbeat {
2149        schema_version: u16,
2150        contract_version: String,
2151        market_id: String,
2152        stream_id: String,
2153        sequence: String,
2154        previous_sequence: String,
2155        server_time_ms: u64,
2156    },
2157}
2158
2159#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2160#[serde(deny_unknown_fields)]
2161pub struct PlatformAccountOrder {
2162    pub order_id: String,
2163    pub side: PlatformTradeSide,
2164    pub order_type: PlatformOrderType,
2165    pub state: PlatformOrderState,
2166    pub limit_price_atoms: String,
2167    pub original_size_atoms: String,
2168    pub remaining_size_atoms: String,
2169}
2170
2171#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2172#[serde(deny_unknown_fields)]
2173pub struct PlatformAccountFill {
2174    pub fill_id: String,
2175    pub side: PlatformTradeSide,
2176    pub price_atoms: String,
2177    pub size_atoms: String,
2178    pub fee_quote_atoms: String,
2179    pub fee_is_final: bool,
2180    pub settlement: PlatformSettlementState,
2181    pub executed_at_ms: u64,
2182    pub confirmed_at_ms: Option<u64>,
2183    pub transaction_id: Option<String>,
2184    pub realized_pnl_quote_atoms: String,
2185}
2186
2187#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2188#[serde(deny_unknown_fields)]
2189pub struct PlatformAccountSnapshotResponse {
2190    pub schema_version: u16,
2191    pub contract_version: String,
2192    pub market_id: String,
2193    pub wallet_address: String,
2194    pub server_time_ms: u64,
2195    pub orders: Vec<PlatformAccountOrder>,
2196    pub fills: Vec<PlatformAccountFill>,
2197}
2198
2199#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
2200#[serde(rename_all = "snake_case")]
2201pub enum PlatformMakerReputationTier {
2202    Probation,
2203    Bronze,
2204    Silver,
2205    Gold,
2206    Platinum,
2207}
2208
2209#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2210#[serde(deny_unknown_fields)]
2211pub struct PlatformMakerTierProgress {
2212    pub next_tier: Option<PlatformMakerReputationTier>,
2213    pub reputation_score_required: Option<u16>,
2214    pub reputation_score_remaining: u16,
2215    pub quote_requests_required: Option<String>,
2216    pub quote_requests_remaining: String,
2217    pub stake_atoms_required: Option<String>,
2218    pub stake_atoms_remaining: String,
2219    pub tenure_slots_required: Option<String>,
2220    pub tenure_slots_remaining: String,
2221}
2222
2223/// Authenticated, privacy-preserving reliability and participation record for the
2224/// requesting maker. All potentially large counters and atomic quantities are
2225/// decimal strings so JavaScript agents never lose integer precision.
2226#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2227#[serde(deny_unknown_fields)]
2228pub struct PlatformMakerReputationResponse {
2229    pub schema_version: u16,
2230    pub contract_version: String,
2231    pub market_id: String,
2232    pub maker_id: String,
2233    pub wallet_address: String,
2234    pub active: bool,
2235    pub tier: PlatformMakerReputationTier,
2236    pub reputation_score: u16,
2237    pub total_quote_requests: String,
2238    pub successful_fills: String,
2239    pub missed_quote_requests: String,
2240    pub fill_rate_bps: u16,
2241    pub consecutive_misses: u16,
2242    pub lifetime_filled_quote_atoms: String,
2243    pub distinct_counterparties: u16,
2244    pub recent_average_latency_ms: u16,
2245    pub configured_minimum_spread_bps: u16,
2246    pub weighted_average_spread_bps: u16,
2247    pub stake_atoms: String,
2248    pub epoch_start_stake_atoms: String,
2249    pub epoch_slashed_atoms: String,
2250    pub epoch_slashed_bps: u16,
2251    pub lifetime_auto_slashed_atoms: String,
2252    pub registered_slot: String,
2253    pub last_active_slot: String,
2254    pub last_settled_slot: String,
2255    pub revoked_at_slot: Option<String>,
2256    pub tenure_slots: String,
2257    pub signed_quote_stream_eligible: bool,
2258    pub minimum_quote_interval_ms: Option<u16>,
2259    pub tier_progress: PlatformMakerTierProgress,
2260    pub server_time_ms: u64,
2261}
2262
2263#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
2264#[serde(rename_all = "snake_case")]
2265pub enum PlatformMakerSide {
2266    Buy,
2267    Sell,
2268}
2269
2270#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
2271#[serde(rename_all = "snake_case")]
2272pub enum PlatformOracleHealth {
2273    Fresh,
2274    Stale,
2275    Unknown,
2276}
2277
2278/// The maker's resting firm orders in this market, summarised by side.
2279#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2280#[serde(deny_unknown_fields)]
2281pub struct PlatformMakerFirmOrderSummary {
2282    pub resting_orders: u32,
2283    pub bid_orders: u32,
2284    pub ask_orders: u32,
2285    pub bid_size_atoms: String,
2286    pub ask_size_atoms: String,
2287}
2288
2289/// One of the maker's own live signed quotes in the streaming lane.
2290#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2291#[serde(deny_unknown_fields)]
2292pub struct PlatformMakerSignedQuote {
2293    pub side: PlatformMakerSide,
2294    pub price_atoms: String,
2295    pub size_atoms: String,
2296    pub nonce: String,
2297    pub issued_at_ms: u64,
2298    pub expires_at_ms: u64,
2299}
2300
2301/// The maker's own intent product in this market.
2302#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2303#[serde(deny_unknown_fields)]
2304pub struct PlatformMakerIntentStatus {
2305    pub active: bool,
2306    pub side: PlatformMakerSide,
2307    pub minimum_price_atoms: String,
2308    pub maximum_price_atoms: String,
2309    pub maximum_fill_size_atoms: String,
2310    /// Fill budget still available after in-flight reservations.
2311    pub remaining_fill_size_atoms: String,
2312    pub minimum_spread_bps: u16,
2313    pub stake_atoms: String,
2314}
2315
2316/// The maker's signed-quote lane: eligibility and its own live quotes.
2317#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2318#[serde(deny_unknown_fields)]
2319pub struct PlatformMakerSignedQuoteLane {
2320    pub eligible: bool,
2321    pub live_quotes: Vec<PlatformMakerSignedQuote>,
2322}
2323
2324#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2325#[serde(deny_unknown_fields)]
2326pub struct PlatformMakerStrandLevel {
2327    /// Null when the configured offset overflows the price range.
2328    pub price_atoms: Option<String>,
2329    pub size_atoms: String,
2330    pub remaining_size_atoms: String,
2331}
2332
2333/// One of the maker's own Strands in this market.
2334#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2335#[serde(deny_unknown_fields)]
2336pub struct PlatformMakerStrandStatus {
2337    pub enabled: bool,
2338    pub async_only: bool,
2339    /// True once the chain would reject fills because `valid_until_slot` passed.
2340    pub expired: bool,
2341    pub mid_price_atoms: String,
2342    pub tick_size_atoms: String,
2343    /// Null means the Strand never expires.
2344    pub valid_until_slot: Option<String>,
2345    pub bids: Vec<PlatformMakerStrandLevel>,
2346    pub asks: Vec<PlatformMakerStrandLevel>,
2347    pub maximum_exposure_atoms: String,
2348    pub remaining_exposure_atoms: String,
2349}
2350
2351/// One of the maker's own Currents in this market.
2352#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2353#[serde(deny_unknown_fields)]
2354pub struct PlatformMakerCurrentStatus {
2355    pub enabled: bool,
2356    pub async_only: bool,
2357    pub expired: bool,
2358    pub half_spread_bps: u16,
2359    pub band_step_bps: u16,
2360    pub maximum_confidence_bps: u16,
2361    pub maximum_oracle_age_seconds: u32,
2362    pub sync_spread_bps: u16,
2363    /// Null means the Current never expires.
2364    pub valid_until_slot: Option<String>,
2365    pub bid_depth_atoms: Vec<String>,
2366    pub ask_depth_atoms: Vec<String>,
2367    pub maximum_exposure_atoms: String,
2368    pub remaining_exposure_atoms: String,
2369    /// Freshness class of the live Strata mark used to price this Current.
2370    pub oracle_health: PlatformOracleHealth,
2371}
2372
2373/// One durable dead-man guard the owner armed for a session in this market.
2374#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2375#[serde(deny_unknown_fields)]
2376pub struct PlatformMakerDeadManGuard {
2377    pub session_public_key: String,
2378    pub status: PlatformDeadManStatus,
2379    pub timeout_ms: u64,
2380    pub heartbeat_deadline_ms: u64,
2381    pub updated_at_ms: u64,
2382}
2383
2384/// Authenticated, owner-scoped view of the maker's Strata products in one
2385/// market: firm orders, intent, Strands, Currents, the signed-quote lane, live
2386/// exposure, health, and kill state. Nothing about other makers, takers, or
2387/// liquidity sources crosses this boundary.
2388#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2389#[serde(deny_unknown_fields)]
2390pub struct PlatformMakerStatusResponse {
2391    pub schema_version: u16,
2392    pub contract_version: String,
2393    pub market_id: String,
2394    pub maker_id: String,
2395    pub wallet_address: String,
2396    pub server_time_ms: u64,
2397    pub current_slot: String,
2398    pub firm_orders: PlatformMakerFirmOrderSummary,
2399    pub intent: Option<PlatformMakerIntentStatus>,
2400    pub signed_quotes: PlatformMakerSignedQuoteLane,
2401    pub strands: Vec<PlatformMakerStrandStatus>,
2402    pub currents: Vec<PlatformMakerCurrentStatus>,
2403    pub dead_man_guards: Vec<PlatformMakerDeadManGuard>,
2404    /// Count of maker products currently able to fill: an active intent, each
2405    /// enabled unexpired Strand or Current, and resting firm orders (as one).
2406    pub active_products: u16,
2407}
2408
2409/// One maker-owned Strand mutation. Amounts that may exceed JavaScript's safe
2410/// integer range remain canonical unsigned decimal strings on the wire.
2411#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2412#[serde(tag = "action", rename_all = "snake_case", deny_unknown_fields)]
2413pub enum PlatformMakerStrandPrepareRequest {
2414    Upsert {
2415        maker_wallet: String,
2416        enabled: bool,
2417        async_only: bool,
2418        sync_spread_ticks: u16,
2419        mid_price_atoms: String,
2420        #[serde(alias = "max_exposure_base_lots")]
2421        max_exposure_base_atoms: String,
2422        bid_offsets_ticks: Vec<u16>,
2423        ask_offsets_ticks: Vec<u16>,
2424        #[serde(alias = "bid_sizes_base_lots")]
2425        bid_sizes_base_atoms: Vec<String>,
2426        #[serde(alias = "ask_sizes_base_lots")]
2427        ask_sizes_base_atoms: Vec<String>,
2428        valid_until_slot: String,
2429    },
2430    Recenter {
2431        maker_wallet: String,
2432        new_mid_price_atoms: String,
2433        valid_until_slot: String,
2434    },
2435    SetEnabled {
2436        maker_wallet: String,
2437        enabled: bool,
2438    },
2439    Cancel {
2440        maker_wallet: String,
2441    },
2442}
2443
2444/// One maker-owned Current mutation. Current is parameterized around the
2445/// market's live Strata mark and therefore has no recenter action.
2446#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2447#[serde(tag = "action", rename_all = "snake_case", deny_unknown_fields)]
2448pub enum PlatformMakerCurrentPrepareRequest {
2449    Upsert {
2450        maker_wallet: String,
2451        enabled: bool,
2452        async_only: bool,
2453        half_spread_bps: u16,
2454        band_step_bps: u16,
2455        max_conf_bps: u16,
2456        max_oracle_dev_bps: u16,
2457        max_oracle_age_secs: u32,
2458        sync_spread_bps: u16,
2459        max_exposure_base_atoms: String,
2460        bid_depth_base_atoms: Vec<String>,
2461        ask_depth_base_atoms: Vec<String>,
2462        valid_until_slot: String,
2463    },
2464    Cancel {
2465        maker_wallet: String,
2466    },
2467}
2468
2469#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
2470#[serde(rename_all = "snake_case")]
2471pub enum PlatformMakerControlProduct {
2472    Strand,
2473    Current,
2474}
2475
2476#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
2477#[serde(rename_all = "snake_case")]
2478pub enum PlatformMakerControlAction {
2479    StrandUpsert,
2480    StrandRecenter,
2481    StrandSetEnabled,
2482    StrandCancel,
2483    CurrentUpsert,
2484    CurrentCancel,
2485}
2486
2487#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2488#[serde(deny_unknown_fields)]
2489pub struct PlatformMakerControlPrepareResponse {
2490    pub schema_version: u16,
2491    pub contract_version: String,
2492    pub maker_control_id: String,
2493    pub market_id: String,
2494    pub maker_wallet: String,
2495    pub product: PlatformMakerControlProduct,
2496    pub action: PlatformMakerControlAction,
2497    /// Unsigned Solana transaction in the format negotiated by the prepare
2498    /// endpoint. The maker verifies the exact instruction and fills its only
2499    /// signature slot externally.
2500    pub transaction_base64: String,
2501    pub recent_blockhash: String,
2502    pub last_valid_block_height: u64,
2503    pub expires_at_ms: u64,
2504}
2505
2506#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2507#[serde(deny_unknown_fields)]
2508pub struct PlatformMakerControlSubmitRequest {
2509    pub maker_control_id: String,
2510    pub signed_transaction_base64: String,
2511    pub idempotency_key: String,
2512}
2513
2514#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
2515#[serde(rename_all = "snake_case")]
2516pub enum PlatformMakerControlSubmissionStatus {
2517    Submitted,
2518}
2519
2520#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2521#[serde(deny_unknown_fields)]
2522pub struct PlatformMakerControlSubmitResponse {
2523    pub schema_version: u16,
2524    pub contract_version: String,
2525    pub maker_control_id: String,
2526    pub market_id: String,
2527    pub maker_wallet: String,
2528    pub product: PlatformMakerControlProduct,
2529    pub action: PlatformMakerControlAction,
2530    pub signature: String,
2531    pub status: PlatformMakerControlSubmissionStatus,
2532}
2533
2534/// Which Strata maker product produced a maker-side fill.
2535#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
2536#[serde(rename_all = "snake_case")]
2537pub enum PlatformMakerProduct {
2538    FirmOrder,
2539    Intent,
2540    Strand,
2541    Current,
2542}
2543
2544/// One maker-side fill: the same sanitized settlement view as an account fill
2545/// plus the maker product that produced it. No counterparty or venue.
2546#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2547#[serde(deny_unknown_fields)]
2548pub struct PlatformMakerFill {
2549    pub fill_id: String,
2550    pub product: PlatformMakerProduct,
2551    pub side: PlatformTradeSide,
2552    pub price_atoms: String,
2553    pub size_atoms: String,
2554    pub fee_quote_atoms: String,
2555    pub fee_is_final: bool,
2556    pub settlement: PlatformSettlementState,
2557    pub executed_at_ms: u64,
2558    pub confirmed_at_ms: Option<u64>,
2559    pub transaction_id: Option<String>,
2560    pub realized_pnl_quote_atoms: String,
2561}
2562
2563/// Authenticated, sequenced owner-only maker stream (`mm.fills.stream`).
2564/// After the signed challenge the server sends one `maker_snapshot`, then
2565/// sequenced `maker_fill`, `maker_status` (exposure/product change), and
2566/// `heartbeat` events; a recovery snapshot advances the sequence on the same
2567/// stream identity.
2568#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2569#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
2570pub enum PlatformMakerEvent {
2571    AuthChallenge {
2572        schema_version: u16,
2573        contract_version: String,
2574        market_id: String,
2575        wallet_address: String,
2576        challenge: String,
2577        server_time_ms: u64,
2578        expires_at_ms: u64,
2579    },
2580    MakerSnapshot {
2581        schema_version: u16,
2582        contract_version: String,
2583        market_id: String,
2584        wallet_address: String,
2585        stream_id: String,
2586        sequence: String,
2587        server_time_ms: u64,
2588        status: PlatformMakerStatusResponse,
2589        fills: Vec<PlatformMakerFill>,
2590    },
2591    MakerFill {
2592        schema_version: u16,
2593        contract_version: String,
2594        market_id: String,
2595        wallet_address: String,
2596        stream_id: String,
2597        sequence: String,
2598        previous_sequence: String,
2599        server_time_ms: u64,
2600        fill: PlatformMakerFill,
2601    },
2602    MakerStatus {
2603        schema_version: u16,
2604        contract_version: String,
2605        market_id: String,
2606        wallet_address: String,
2607        stream_id: String,
2608        sequence: String,
2609        previous_sequence: String,
2610        server_time_ms: u64,
2611        status: PlatformMakerStatusResponse,
2612    },
2613    Heartbeat {
2614        schema_version: u16,
2615        contract_version: String,
2616        market_id: String,
2617        wallet_address: String,
2618        stream_id: String,
2619        sequence: String,
2620        previous_sequence: String,
2621        server_time_ms: u64,
2622    },
2623}
2624
2625#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2626#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
2627pub enum PlatformAccountEvent {
2628    AuthChallenge {
2629        schema_version: u16,
2630        contract_version: String,
2631        market_id: String,
2632        wallet_address: String,
2633        challenge: String,
2634        server_time_ms: u64,
2635        expires_at_ms: u64,
2636    },
2637    AccountSnapshot {
2638        schema_version: u16,
2639        contract_version: String,
2640        market_id: String,
2641        wallet_address: String,
2642        stream_id: String,
2643        sequence: String,
2644        server_time_ms: u64,
2645        orders: Vec<PlatformAccountOrder>,
2646        fills: Vec<PlatformAccountFill>,
2647    },
2648    OrdersSnapshot {
2649        schema_version: u16,
2650        contract_version: String,
2651        market_id: String,
2652        wallet_address: String,
2653        stream_id: String,
2654        sequence: String,
2655        previous_sequence: String,
2656        server_time_ms: u64,
2657        orders: Vec<PlatformAccountOrder>,
2658    },
2659    Fill {
2660        schema_version: u16,
2661        contract_version: String,
2662        market_id: String,
2663        wallet_address: String,
2664        stream_id: String,
2665        sequence: String,
2666        previous_sequence: String,
2667        server_time_ms: u64,
2668        fill: PlatformAccountFill,
2669    },
2670    Heartbeat {
2671        schema_version: u16,
2672        contract_version: String,
2673        market_id: String,
2674        wallet_address: String,
2675        stream_id: String,
2676        sequence: String,
2677        previous_sequence: String,
2678        server_time_ms: u64,
2679    },
2680}
2681
2682#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2683#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
2684pub enum PlatformMarketDataEvent {
2685    BookSnapshot {
2686        schema_version: u16,
2687        contract_version: String,
2688        market_id: String,
2689        stream_id: String,
2690        sequence: String,
2691        server_time_ms: u64,
2692        snapshot_id: String,
2693        bids: Vec<PlatformBookLevel>,
2694        asks: Vec<PlatformBookLevel>,
2695    },
2696    BookDelta {
2697        schema_version: u16,
2698        contract_version: String,
2699        market_id: String,
2700        stream_id: String,
2701        sequence: String,
2702        previous_sequence: String,
2703        server_time_ms: u64,
2704        changes: Vec<PlatformBookChange>,
2705    },
2706    BestBidAsk {
2707        schema_version: u16,
2708        contract_version: String,
2709        market_id: String,
2710        stream_id: String,
2711        sequence: String,
2712        server_time_ms: u64,
2713        best_bid: Option<PlatformBookLevel>,
2714        best_ask: Option<PlatformBookLevel>,
2715    },
2716    Trade {
2717        schema_version: u16,
2718        contract_version: String,
2719        market_id: String,
2720        server_time_ms: u64,
2721        trade: PlatformTrade,
2722    },
2723    MarketStatus {
2724        schema_version: u16,
2725        contract_version: String,
2726        market_id: String,
2727        server_time_ms: u64,
2728        status: PlatformMarketState,
2729    },
2730    Heartbeat {
2731        schema_version: u16,
2732        contract_version: String,
2733        market_id: String,
2734        server_time_ms: u64,
2735    },
2736}
2737
2738#[cfg(test)]
2739mod tests {
2740    use super::*;
2741
2742    #[test]
2743    fn public_platform_fixtures_decode_strictly() {
2744        let discovery: PlatformDiscoveryResponse =
2745            serde_json::from_str(PLATFORM_CAPABILITIES_FIXTURE).unwrap();
2746        let service_status: PlatformServiceStatusResponse =
2747            serde_json::from_str(PLATFORM_SERVICE_STATUS_FIXTURE).unwrap();
2748        let graph = PlatformActionGraphResponse::foundation();
2749        let assets: PlatformAssetsResponse = serde_json::from_str(PLATFORM_ASSETS_FIXTURE).unwrap();
2750        let swap_quote: PlatformSwapQuoteResponse =
2751            serde_json::from_str(PLATFORM_SWAP_QUOTE_FIXTURE).unwrap();
2752        let markets: PlatformMarketsResponse =
2753            serde_json::from_str(PLATFORM_MARKETS_FIXTURE).unwrap();
2754        let book: PlatformBookSnapshotResponse =
2755            serde_json::from_str(PLATFORM_BOOK_FIXTURE).unwrap();
2756        let bbo: PlatformBestBidAskResponse = serde_json::from_str(PLATFORM_BBO_FIXTURE).unwrap();
2757        let fees: PlatformFeeScheduleResponse =
2758            serde_json::from_str(PLATFORM_FEES_FIXTURE).unwrap();
2759        let status: PlatformMarketStatusResponse =
2760            serde_json::from_str(PLATFORM_STATUS_FIXTURE).unwrap();
2761        let candles: PlatformCandlesResponse =
2762            serde_json::from_str(PLATFORM_CANDLES_FIXTURE).unwrap();
2763        let mark: PlatformMarkResponse = serde_json::from_str(PLATFORM_MARK_FIXTURE).unwrap();
2764        let execution_status: PlatformExecutionStatusResponse =
2765            serde_json::from_str(PLATFORM_EXECUTION_STATUS_FIXTURE).unwrap();
2766        let twaps: PlatformTwapsResponse = serde_json::from_str(PLATFORM_TWAPS_FIXTURE).unwrap();
2767        let twap_challenge: PlatformTwapChallengeResponse =
2768            serde_json::from_str(PLATFORM_TWAP_CHALLENGE_FIXTURE).unwrap();
2769        let twap_prepare: PlatformTwapPrepareResponse =
2770            serde_json::from_str(PLATFORM_TWAP_PREPARE_FIXTURE).unwrap();
2771        let twap_submit: PlatformTwapSubmitResponse =
2772            serde_json::from_str(PLATFORM_TWAP_SUBMIT_FIXTURE).unwrap();
2773        let portfolio_history: PlatformPortfolioHistoryResponse =
2774            serde_json::from_str(PLATFORM_PORTFOLIO_HISTORY_FIXTURE).unwrap();
2775        let portfolio: PlatformPortfolioResponse =
2776            serde_json::from_str(PLATFORM_PORTFOLIO_FIXTURE).unwrap();
2777        let rewards: PlatformRewardsResponse =
2778            serde_json::from_str(PLATFORM_REWARDS_FIXTURE).unwrap();
2779        let referrals: PlatformReferralsResponse =
2780            serde_json::from_str(PLATFORM_REFERRALS_FIXTURE).unwrap();
2781        let referral_link: PlatformReferralLinkResponse =
2782            serde_json::from_str(PLATFORM_REFERRAL_LINK_FIXTURE).unwrap();
2783        let referral_claim: PlatformReferralClaimResponse =
2784            serde_json::from_str(PLATFORM_REFERRAL_CLAIM_FIXTURE).unwrap();
2785        let vault_status: PlatformVaultStatusResponse =
2786            serde_json::from_str(PLATFORM_VAULT_STATUS_FIXTURE).unwrap();
2787        let vault_pause: PlatformVaultPausePrepareResponse =
2788            serde_json::from_str(PLATFORM_VAULT_PAUSE_PREPARE_FIXTURE).unwrap();
2789        let vault_setup: PlatformVaultSetupPrepareResponse =
2790            serde_json::from_str(PLATFORM_VAULT_SETUP_PREPARE_FIXTURE).unwrap();
2791        let vault_delegate: PlatformVaultDelegatePrepareResponse =
2792            serde_json::from_str(PLATFORM_VAULT_DELEGATE_PREPARE_FIXTURE).unwrap();
2793        let vault_policy: PlatformVaultPolicyPrepareResponse =
2794            serde_json::from_str(PLATFORM_VAULT_POLICY_PREPARE_FIXTURE).unwrap();
2795        let vault_deposit: PlatformVaultDepositPrepareResponse =
2796            serde_json::from_str(PLATFORM_VAULT_DEPOSIT_PREPARE_FIXTURE).unwrap();
2797        let vault_withdraw: PlatformVaultWithdrawPrepareResponse =
2798            serde_json::from_str(PLATFORM_VAULT_WITHDRAW_PREPARE_FIXTURE).unwrap();
2799        let vault_submit: PlatformVaultSubmitResponse =
2800            serde_json::from_str(PLATFORM_VAULT_SUBMIT_FIXTURE).unwrap();
2801        let bugs: PlatformBugsResponse = serde_json::from_str(PLATFORM_BUGS_FIXTURE).unwrap();
2802        let bug_submit: PlatformBugSubmitResponse =
2803            serde_json::from_str(PLATFORM_BUG_SUBMIT_FIXTURE).unwrap();
2804        let trades: PlatformTradesResponse = serde_json::from_str(PLATFORM_TRADES_FIXTURE).unwrap();
2805        let account: PlatformAccountSnapshotResponse =
2806            serde_json::from_str(PLATFORM_ACCOUNT_FIXTURE).unwrap();
2807        let maker_reputation: PlatformMakerReputationResponse =
2808            serde_json::from_str(PLATFORM_MAKER_REPUTATION_FIXTURE).unwrap();
2809        let maker_status: PlatformMakerStatusResponse =
2810            serde_json::from_str(PLATFORM_MAKER_STATUS_FIXTURE).unwrap();
2811        let maker_stream: PlatformMakerEvent =
2812            serde_json::from_str(PLATFORM_MAKER_STREAM_FIXTURE).unwrap();
2813        let twap_stream: PlatformTwapEvent =
2814            serde_json::from_str(PLATFORM_TWAP_STREAM_FIXTURE).unwrap();
2815        let execution_stream: PlatformExecutionEvent =
2816            serde_json::from_str(PLATFORM_EXECUTION_STREAM_FIXTURE).unwrap();
2817        let order_challenge: PlatformOrderChallengeResponse =
2818            serde_json::from_str(PLATFORM_ORDER_CHALLENGE_FIXTURE).unwrap();
2819        let order_prepare: PlatformOrderPrepareResponse =
2820            serde_json::from_str(PLATFORM_ORDER_PREPARE_FIXTURE).unwrap();
2821        let order_submit: PlatformOrderSubmitResponse =
2822            serde_json::from_str(PLATFORM_ORDER_SUBMIT_FIXTURE).unwrap();
2823        let order_status: PlatformOrderStatusResponse =
2824            serde_json::from_str(PLATFORM_ORDER_STATUS_FIXTURE).unwrap();
2825
2826        assert_eq!(discovery.schema_version, PLATFORM_SCHEMA_VERSION);
2827        assert_eq!(service_status.status, PlatformServiceState::Operational);
2828        assert_eq!(service_status.available_operations, 59);
2829        assert_eq!(graph.entry_operation_id, "platform.capabilities.read");
2830        assert_eq!(graph.operations.len(), 69);
2831        assert_eq!(maker_reputation.tier, PlatformMakerReputationTier::Gold);
2832        assert_eq!(maker_status.active_products, 3);
2833        match &maker_stream {
2834            PlatformMakerEvent::MakerSnapshot { status, fills, .. } => {
2835                assert_eq!(status.active_products, maker_status.active_products);
2836                assert_eq!(fills.len(), 1);
2837                assert_eq!(fills[0].product, PlatformMakerProduct::Strand);
2838            }
2839            other => panic!("maker stream fixture must be a snapshot, got {other:?}"),
2840        }
2841        match &twap_stream {
2842            PlatformTwapEvent::TwapsSnapshot {
2843                twaps: streamed, ..
2844            } => {
2845                assert_eq!(streamed, &twaps.twaps);
2846            }
2847            other => panic!("twap stream fixture must be a snapshot, got {other:?}"),
2848        }
2849        match &execution_stream {
2850            PlatformExecutionEvent::ExecutionsSnapshot {
2851                executions,
2852                unknown_execution_ids,
2853                ..
2854            } => {
2855                assert_eq!(executions.len(), 2);
2856                assert_eq!(executions[0].execution_id, execution_status.execution_id);
2857                assert_eq!(unknown_execution_ids.len(), 1);
2858            }
2859            other => panic!("execution stream fixture must be a snapshot, got {other:?}"),
2860        }
2861        assert_eq!(maker_status.strands.len(), 1);
2862        assert_eq!(maker_status.currents.len(), 1);
2863        assert!(maker_status
2864            .intent
2865            .as_ref()
2866            .is_some_and(|intent| intent.active));
2867        assert_eq!(portfolio.balances.len(), 2);
2868        assert_eq!(portfolio.positions.len(), 1);
2869        assert!(portfolio.valuation_complete);
2870        assert_eq!(portfolio.equity_usd_micros.as_deref(), Some("439989500"));
2871        assert!(graph
2872            .operations
2873            .iter()
2874            .any(|operation| operation.id == "twap.place.submit"));
2875        assert!(graph
2876            .operations
2877            .iter()
2878            .any(|operation| operation.id == "twap.cancel.submit"));
2879        assert_eq!(discovery.capabilities.len(), 5);
2880        assert!(!discovery.authority.accepts_private_keys);
2881        assert_eq!(assets.assets.len(), 2);
2882        assert_eq!(swap_quote.input_asset_id, assets.assets[0].asset_id);
2883        assert_eq!(swap_quote.output_asset_id, assets.assets[1].asset_id);
2884        assert_eq!(markets.markets.len(), 1);
2885        assert_eq!(markets.markets[0].base_asset_id, assets.assets[0].asset_id);
2886        assert_eq!(markets.markets[0].quote_asset_id, assets.assets[1].asset_id);
2887        assert_eq!(book.sequence, "42");
2888        assert_eq!(bbo.best_bid.unwrap().price_atoms, "149990000");
2889        assert_eq!(fees.maximum_immediate_execution_fee_bps, 10);
2890        assert_eq!(status.status, PlatformMarketState::Active);
2891        assert_eq!(candles.candles.len(), 2);
2892        assert_eq!(mark.price_atoms_per_base_unit.as_deref(), Some("149995000"));
2893        assert_eq!(execution_status.status, PlatformExecutionState::Confirmed);
2894        assert_eq!(
2895            execution_status.settlement,
2896            PlatformSettlementState::Confirmed
2897        );
2898        assert_eq!(twaps.twaps[0].fills.len(), 1);
2899        assert_eq!(twaps.twaps[0].slices_executed, 2);
2900        assert_eq!(twap_challenge.action, PlatformTwapControlAction::Place);
2901        assert_eq!(twap_prepare.twap_id, twap_challenge.twap_id);
2902        assert_eq!(twap_submit.twap_control_id, twap_prepare.twap_control_id);
2903        assert_eq!(portfolio_history.points.len(), 2);
2904        assert_eq!(rewards.standings.len(), 2);
2905        assert!(referrals.enabled);
2906        assert_eq!(referral_link.status, "pending_first_fill");
2907        assert_eq!(referral_claim.status, "requested");
2908        assert_eq!(vault_status.state, PlatformVaultState::Active);
2909        assert_eq!(
2910            vault_status.session.as_ref().unwrap().state,
2911            PlatformVaultSessionState::Active
2912        );
2913        assert!(vault_pause.paused);
2914        assert!(vault_pause.owner_signature_required);
2915        assert_eq!(vault_setup.mode, PlatformVaultSetupMode::Create);
2916        assert!(vault_setup.permanent);
2917        assert_eq!(vault_delegate.action, PlatformVaultDelegateAction::Revoke);
2918        assert!(vault_delegate.owner_signature_required);
2919        assert_eq!(
2920            vault_policy.withdrawal_access.mode,
2921            PlatformVaultWithdrawalMode::Restricted
2922        );
2923        assert!(vault_policy.owner_signature_required);
2924        assert_eq!(vault_deposit.amount_atoms, "10000000");
2925        assert!(vault_deposit.owner_signature_required);
2926        assert_eq!(vault_withdraw.amount_atoms, "5000000");
2927        assert!(vault_withdraw.owner_signature_required);
2928        assert!(vault_withdraw.sponsored);
2929        assert!(vault_withdraw.preparation_id.starts_with("vp_"));
2930        assert_eq!(vault_submit.action, PlatformVaultAction::Deposit);
2931        assert_eq!(
2932            vault_submit.status,
2933            PlatformVaultSubmissionStatus::Submitted
2934        );
2935        assert!(vault_submit.sponsored);
2936        assert_eq!(vault_submit.failure_code, None);
2937        assert_eq!(bugs.reports[0].status, PlatformBugStatus::Confirmed);
2938        assert_eq!(bug_submit.status, PlatformBugStatus::Pending);
2939        assert_eq!(trades.trades.len(), 1);
2940        assert_eq!(account.orders.len(), 1);
2941        assert_eq!(account.fills.len(), 1);
2942        assert_eq!(order_challenge.action, PlatformOrderAction::Place);
2943        assert_eq!(order_prepare.order_ids, order_challenge.order_ids);
2944        assert_eq!(order_submit.order_ids, order_challenge.order_ids);
2945        assert_eq!(order_status.order_control_id, order_submit.order_control_id);
2946        assert_eq!(order_status.status, PlatformOrderControlStatus::Submitting);
2947    }
2948
2949    #[test]
2950    fn public_platform_response_rejects_unreviewed_fields() {
2951        let mut value: serde_json::Value =
2952            serde_json::from_str(PLATFORM_CAPABILITIES_FIXTURE).unwrap();
2953        value
2954            .as_object_mut()
2955            .unwrap()
2956            .insert("unexpected_field".to_owned(), serde_json::Value::Bool(true));
2957        assert!(serde_json::from_value::<PlatformDiscoveryResponse>(value).is_err());
2958
2959        let mut account_event: serde_json::Value =
2960            serde_json::from_str(PLATFORM_ACCOUNT_FIXTURE).unwrap();
2961        let event = account_event.as_object_mut().unwrap();
2962        event.insert("type".to_owned(), serde_json::json!("account_snapshot"));
2963        event.insert(
2964            "stream_id".to_owned(),
2965            serde_json::json!("account_stream_66666666666666666666666666666666"),
2966        );
2967        event.insert("sequence".to_owned(), serde_json::json!("1"));
2968        event.insert("unexpected_field".to_owned(), serde_json::json!(true));
2969        assert!(serde_json::from_value::<PlatformAccountEvent>(account_event).is_err());
2970    }
2971
2972    #[test]
2973    fn platform_graph_availability_is_projected_from_live_capabilities() {
2974        let mut graph = PlatformActionGraphResponse::foundation();
2975        let live = std::collections::BTreeSet::from([
2976            "platform.discover".to_owned(),
2977            "graphs.read".to_owned(),
2978            "orders.replace".to_owned(),
2979        ]);
2980
2981        graph.project_availability(&live);
2982
2983        for operation in &graph.operations {
2984            assert_eq!(
2985                operation.available,
2986                live.contains(&operation.capability_id),
2987                "operation {} did not follow capability {}",
2988                operation.id,
2989                operation.capability_id,
2990            );
2991        }
2992        assert!(graph
2993            .workflows
2994            .iter()
2995            .flat_map(|workflow| &workflow.nodes)
2996            .filter(|node| node.kind != PlatformActionKind::ExternalSignature)
2997            .all(|node| {
2998                node.available
2999                    == node
3000                        .capability_id
3001                        .as_ref()
3002                        .is_some_and(|capability_id| live.contains(capability_id))
3003            }));
3004        assert!(graph
3005            .workflows
3006            .iter()
3007            .flat_map(|workflow| &workflow.nodes)
3008            .filter(|node| node.kind == PlatformActionKind::ExternalSignature)
3009            .all(|node| node.available));
3010    }
3011
3012    #[test]
3013    fn atomic_order_batch_request_is_strict_and_typed() {
3014        let request: PlatformOrderChallengeRequest = serde_json::from_value(serde_json::json!({
3015            "action": "batch",
3016            "owner_wallet": "11111111111111111111111111111111",
3017            "session_public_key": "22222222222222222222222222222222",
3018            "operations": [
3019                {
3020                    "action": "cancel",
3021                    "order_id": "order_11111111111111111111111111111111"
3022                },
3023                {
3024                    "action": "replace",
3025                    "order_id": "order_22222222222222222222222222222222",
3026                    "account_sequence": "8",
3027                    "client_order_id": "replacement-8",
3028                    "side": "sell",
3029                    "order_type": "post_only",
3030                    "limit_price_atoms": "151000000",
3031                    "size_atoms": "2000000"
3032                }
3033            ]
3034        }))
3035        .unwrap();
3036        let PlatformOrderChallengeRequest::Batch { operations, .. } = request else {
3037            panic!("expected batch request");
3038        };
3039        assert_eq!(operations.len(), 2);
3040        assert!(matches!(
3041            &operations[1],
3042            PlatformOrderBatchOperation::Replace { account_sequence: Some(sequence), .. }
3043                if sequence == "8"
3044        ));
3045
3046        // The account sequence is optional: Strata resolves it from the Vault's
3047        // confirmed market account when omitted, and omitted stays omitted on
3048        // the wire so older servers reject rather than misread it.
3049        let place: PlatformOrderChallengeRequest = serde_json::from_value(serde_json::json!({
3050            "action": "place",
3051            "owner_wallet": "11111111111111111111111111111111",
3052            "session_public_key": "22222222222222222222222222222222",
3053            "client_order_id": "first-order",
3054            "side": "buy",
3055            "order_type": "post_only",
3056            "limit_price_atoms": "150000000",
3057            "size_atoms": "1000000"
3058        }))
3059        .unwrap();
3060        assert!(matches!(
3061            place,
3062            PlatformOrderChallengeRequest::Place {
3063                account_sequence: None,
3064                ..
3065            }
3066        ));
3067        assert!(!serde_json::to_string(&place)
3068            .unwrap()
3069            .contains("account_sequence"));
3070
3071        assert!(
3072            serde_json::from_value::<PlatformOrderChallengeRequest>(serde_json::json!({
3073                "action": "batch",
3074                "owner_wallet": "11111111111111111111111111111111",
3075                "session_public_key": "22222222222222222222222222222222",
3076                "operations": [{
3077                    "action": "cancel",
3078                    "order_id": "order_11111111111111111111111111111111",
3079                    "implementation": "hidden"
3080                }]
3081            }))
3082            .is_err()
3083        );
3084    }
3085
3086    #[test]
3087    fn persistent_order_commands_are_strict_and_explicit_about_self_trade_policy() {
3088        let frame: PlatformOrderCommandClientFrame = serde_json::from_value(serde_json::json!({
3089            "type": "command",
3090            "request_id": "agent-1",
3091            "sequence": "1",
3092            "command": {
3093                "type": "challenge",
3094                "self_trade_prevention": "cancel_maker",
3095                "request": {
3096                    "action": "cancel_all",
3097                    "owner_wallet": "11111111111111111111111111111111",
3098                    "session_public_key": "22222222222222222222222222222222"
3099                }
3100            }
3101        }))
3102        .unwrap();
3103        assert!(matches!(
3104            frame,
3105            PlatformOrderCommandClientFrame::Command {
3106                command: PlatformOrderCommand::Challenge {
3107                    self_trade_prevention: PlatformSelfTradePrevention::CancelMaker,
3108                    ..
3109                },
3110                ..
3111            }
3112        ));
3113        assert!(
3114            serde_json::from_value::<PlatformOrderCommandClientFrame>(serde_json::json!({
3115                "type": "command",
3116                "request_id": "agent-1",
3117                "sequence": "1",
3118                "command": {
3119                    "type": "challenge",
3120                    "request": {
3121                        "action": "cancel_all",
3122                        "owner_wallet": "11111111111111111111111111111111",
3123                        "session_public_key": "22222222222222222222222222222222"
3124                    }
3125                }
3126            }))
3127            .is_err()
3128        );
3129    }
3130
3131    #[test]
3132    fn prepare_requests_accept_a_signed_challenge_or_the_operation_itself() {
3133        let signed: PlatformOrderPrepareRequest = serde_json::from_value(serde_json::json!({
3134            "challenge_id": "oc_0123456789abcdef0123456789abcdef",
3135            "authorization_signature": "1111",
3136        }))
3137        .unwrap();
3138        assert!(matches!(signed, PlatformOrderPrepareRequest::Authorized(_)));
3139        let direct: PlatformOrderPrepareRequest = serde_json::from_value(serde_json::json!({
3140            "action": "cancel_all",
3141            "owner_wallet": "5Ji61Fbeb22Yntgv1hhHeSSLgdEdZchHeM1Tv1MjGhSL",
3142            "session_public_key": "9Uu7cLBgfMk233BAjMvTS8XJy6KbZK7oQ7NXuCTi3Fg2",
3143        }))
3144        .unwrap();
3145        assert!(matches!(
3146            direct,
3147            PlatformOrderPrepareRequest::Direct(PlatformOrderChallengeRequest::CancelAll { .. })
3148        ));
3149        // Neither shape tolerates a stray field.
3150        assert!(
3151            serde_json::from_value::<PlatformOrderPrepareRequest>(serde_json::json!({
3152                "challenge_id": "oc_0123456789abcdef0123456789abcdef",
3153                "authorization_signature": "1111",
3154                "extra": true,
3155            }))
3156            .is_err()
3157        );
3158        let twap: PlatformTwapPrepareRequest = serde_json::from_value(serde_json::json!({
3159            "action": "cancel",
3160            "owner_wallet": "5Ji61Fbeb22Yntgv1hhHeSSLgdEdZchHeM1Tv1MjGhSL",
3161            "session_public_key": "9Uu7cLBgfMk233BAjMvTS8XJy6KbZK7oQ7NXuCTi3Fg2",
3162            "twap_id": "twap_0123456789abcdef0123456789abcdef",
3163        }))
3164        .unwrap();
3165        assert!(matches!(twap, PlatformTwapPrepareRequest::Direct(_)));
3166        let execution: crate::ExecutionPrepareRequest = serde_json::from_value(serde_json::json!({
3167            "quote_id": "quote_0123456789abcdef0123456789abcdef",
3168            "owner_wallet": "5Ji61Fbeb22Yntgv1hhHeSSLgdEdZchHeM1Tv1MjGhSL",
3169            "session_public_key": "9Uu7cLBgfMk233BAjMvTS8XJy6KbZK7oQ7NXuCTi3Fg2",
3170        }))
3171        .unwrap();
3172        assert!(matches!(
3173            execution,
3174            crate::ExecutionPrepareRequest::Direct(_)
3175        ));
3176    }
3177
3178    #[test]
3179    fn maker_control_requests_are_tagged_exact_and_amount_safe() {
3180        let strand_upsert: PlatformMakerStrandPrepareRequest =
3181            serde_json::from_value(serde_json::json!({
3182                "action": "upsert",
3183                "maker_wallet": "5Ji61Fbeb22Yntgv1hhHeSSLgdEdZchHeM1Tv1MjGhSL",
3184                "enabled": true,
3185                "async_only": false,
3186                "sync_spread_ticks": 1,
3187                "mid_price_atoms": "123000000",
3188                "max_exposure_base_atoms": "1000000",
3189                "bid_offsets_ticks": vec![1; 16],
3190                "ask_offsets_ticks": vec![1; 16],
3191                "bid_sizes_base_atoms": vec!["1"; 16],
3192                "ask_sizes_base_atoms": vec!["1"; 16],
3193                "valid_until_slot": "0"
3194            }))
3195            .unwrap();
3196        let serialized = serde_json::to_value(&strand_upsert).unwrap();
3197        assert_eq!(serialized["max_exposure_base_atoms"], "1000000");
3198        assert!(serialized.get("max_exposure_base_lots").is_none());
3199
3200        // 0.2.1 clients remain accepted, but every response and current client
3201        // uses the corrected base-atom vocabulary.
3202        let legacy_strand: PlatformMakerStrandPrepareRequest =
3203            serde_json::from_value(serde_json::json!({
3204                "action": "upsert",
3205                "maker_wallet": "5Ji61Fbeb22Yntgv1hhHeSSLgdEdZchHeM1Tv1MjGhSL",
3206                "enabled": true,
3207                "async_only": false,
3208                "sync_spread_ticks": 1,
3209                "mid_price_atoms": "123000000",
3210                "max_exposure_base_lots": "1000000",
3211                "bid_offsets_ticks": vec![1; 16],
3212                "ask_offsets_ticks": vec![1; 16],
3213                "bid_sizes_base_lots": vec!["1"; 16],
3214                "ask_sizes_base_lots": vec!["1"; 16],
3215                "valid_until_slot": "0"
3216            }))
3217            .unwrap();
3218        assert_eq!(serde_json::to_value(legacy_strand).unwrap(), serialized);
3219
3220        let strand: PlatformMakerStrandPrepareRequest = serde_json::from_value(serde_json::json!({
3221            "action": "recenter",
3222            "maker_wallet": "5Ji61Fbeb22Yntgv1hhHeSSLgdEdZchHeM1Tv1MjGhSL",
3223            "new_mid_price_atoms": "123000000",
3224            "valid_until_slot": "0"
3225        }))
3226        .unwrap();
3227        assert!(matches!(
3228            strand,
3229            PlatformMakerStrandPrepareRequest::Recenter { .. }
3230        ));
3231
3232        let current: PlatformMakerCurrentPrepareRequest =
3233            serde_json::from_value(serde_json::json!({
3234                "action": "cancel",
3235                "maker_wallet": "5Ji61Fbeb22Yntgv1hhHeSSLgdEdZchHeM1Tv1MjGhSL"
3236            }))
3237            .unwrap();
3238        assert!(matches!(
3239            current,
3240            PlatformMakerCurrentPrepareRequest::Cancel { .. }
3241        ));
3242        assert!(
3243            serde_json::from_value::<PlatformMakerCurrentPrepareRequest>(serde_json::json!({
3244                "action": "cancel",
3245                "maker_wallet": "5Ji61Fbeb22Yntgv1hhHeSSLgdEdZchHeM1Tv1MjGhSL",
3246                "oracle_price": 123.45
3247            }))
3248            .is_err()
3249        );
3250    }
3251}