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. Names the market whose price protection the session pins
1223    /// when the product has one; the session trades every market either way.
1224    #[serde(default)]
1225    pub market_id: Option<String>,
1226    /// Null or absent requests the permanent-session expiry supported by the
1227    /// product.
1228    #[serde(default)]
1229    pub expires_at_ms: Option<u64>,
1230    /// Absent takes `PLATFORM_SESSION_DEFAULT_MINIMUM_INTERVAL_SECONDS`.
1231    #[serde(default)]
1232    pub minimum_interval_seconds: Option<u32>,
1233    /// Absent takes `PLATFORM_SESSION_DEFAULT_MAXIMUM_TOLERANCE_BPS`.
1234    #[serde(default)]
1235    pub maximum_tolerance_bps: Option<u16>,
1236    /// Optional per-asset ceilings, at most `PLATFORM_SESSION_MAX_SPENDING_LIMITS`.
1237    /// Assets without a limit are unlimited.
1238    #[serde(default)]
1239    pub spending_limits: Vec<PlatformVaultSpendingLimit>,
1240}
1241
1242/// Owner-bound onboarding or session-replacement transaction. Product inputs
1243/// are echoed exactly so an external verifier can reject changed intent.
1244#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1245#[serde(deny_unknown_fields)]
1246pub struct PlatformVaultSetupPrepareResponse {
1247    pub schema_version: u16,
1248    pub contract_version: String,
1249    pub server_time_ms: u64,
1250    pub wallet_address: String,
1251    pub session_public_key: String,
1252    /// The market named in the request, if any.
1253    pub market_id: Option<String>,
1254    pub mode: PlatformVaultSetupMode,
1255    pub expires_at_ms: Option<u64>,
1256    pub permanent: bool,
1257    /// The applied policy, defaults resolved.
1258    pub minimum_interval_seconds: u32,
1259    pub maximum_tolerance_bps: u16,
1260    pub spending_limits: Vec<PlatformVaultSpendingLimit>,
1261    pub transaction_base64: String,
1262    pub recent_blockhash: String,
1263    pub owner_signature_required: bool,
1264    /// Opaque handle for this prepared transaction. Hand it back with the
1265    /// owner-signed transaction to `vault.relay` and Strata submits it — no
1266    /// RPC or SOL needed on the owner side.
1267    pub preparation_id: String,
1268    /// `true` when Strata is the transaction fee payer and covers any rent the
1269    /// action creates, so the owner needs no SOL at all. `false` means the
1270    /// owner wallet is the fee payer (Strata still submits it on request).
1271    pub sponsored: bool,
1272    /// The prepared transaction must be submitted before this server time.
1273    pub submit_by_ms: u64,
1274}
1275
1276#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1277#[serde(rename_all = "snake_case")]
1278pub enum PlatformVaultDelegateAction {
1279    Revoke,
1280}
1281
1282#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1283#[serde(deny_unknown_fields)]
1284pub struct PlatformVaultDelegatePrepareRequest {
1285    pub wallet_address: String,
1286    pub session_public_key: String,
1287    pub action: PlatformVaultDelegateAction,
1288}
1289
1290/// Unsigned session-lifecycle control. The owner verifies both identities and
1291/// the destructive action before signing and broadcasting it externally.
1292#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1293#[serde(deny_unknown_fields)]
1294pub struct PlatformVaultDelegatePrepareResponse {
1295    pub schema_version: u16,
1296    pub contract_version: String,
1297    pub server_time_ms: u64,
1298    pub wallet_address: String,
1299    pub session_public_key: String,
1300    pub action: PlatformVaultDelegateAction,
1301    pub transaction_base64: String,
1302    pub recent_blockhash: String,
1303    pub owner_signature_required: bool,
1304    /// Opaque handle for this prepared transaction. Hand it back with the
1305    /// owner-signed transaction to `vault.relay` and Strata submits it — no
1306    /// RPC or SOL needed on the owner side.
1307    pub preparation_id: String,
1308    /// `true` when Strata is the transaction fee payer and covers any rent the
1309    /// action creates, so the owner needs no SOL at all. `false` means the
1310    /// owner wallet is the fee payer (Strata still submits it on request).
1311    pub sponsored: bool,
1312    /// The prepared transaction must be submitted before this server time.
1313    pub submit_by_ms: u64,
1314}
1315
1316#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1317#[serde(deny_unknown_fields)]
1318pub struct PlatformVaultPolicyPrepareRequest {
1319    pub wallet_address: String,
1320    pub withdrawal_access: PlatformVaultWithdrawalAccess,
1321}
1322
1323/// An owner-bound withdrawal-access change. Unrestricted access is a status
1324/// state rather than a preparable action in this contract.
1325#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1326#[serde(deny_unknown_fields)]
1327pub struct PlatformVaultPolicyPrepareResponse {
1328    pub schema_version: u16,
1329    pub contract_version: String,
1330    pub server_time_ms: u64,
1331    pub wallet_address: String,
1332    pub withdrawal_access: PlatformVaultWithdrawalAccess,
1333    pub transaction_base64: String,
1334    pub recent_blockhash: String,
1335    pub owner_signature_required: bool,
1336    /// Opaque handle for this prepared transaction. Hand it back with the
1337    /// owner-signed transaction to `vault.relay` and Strata submits it — no
1338    /// RPC or SOL needed on the owner side.
1339    pub preparation_id: String,
1340    /// `true` when Strata is the transaction fee payer and covers any rent the
1341    /// action creates, so the owner needs no SOL at all. `false` means the
1342    /// owner wallet is the fee payer (Strata still submits it on request).
1343    pub sponsored: bool,
1344    /// The prepared transaction must be submitted before this server time.
1345    pub submit_by_ms: u64,
1346}
1347
1348#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1349#[serde(deny_unknown_fields)]
1350pub struct PlatformVaultDepositPrepareRequest {
1351    pub wallet_address: String,
1352    pub market_id: String,
1353    pub asset_id: String,
1354    pub amount_atoms: String,
1355    /// Optional external session key. When it is not yet registered for this
1356    /// wallet, the same deposit transaction registers it with the default
1357    /// session policy — a first deposit is the whole onboarding, one owner
1358    /// signature. An already-registered key changes nothing.
1359    #[serde(default)]
1360    pub session_public_key: Option<String>,
1361}
1362
1363/// Exact owner-funded deposit transaction. Asset construction and custody
1364/// identities remain internal; the public intent is echoed for verification.
1365#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1366#[serde(deny_unknown_fields)]
1367pub struct PlatformVaultDepositPrepareResponse {
1368    pub schema_version: u16,
1369    pub contract_version: String,
1370    pub server_time_ms: u64,
1371    pub wallet_address: String,
1372    pub market_id: String,
1373    pub asset_id: String,
1374    pub amount_atoms: String,
1375    /// SOL Strata already spent on this owner's sponsored actions, recovered
1376    /// in the deposit asset inside this same transaction (a second transfer
1377    /// from the owner's account to Strata). "0" when nothing is owed. It is
1378    /// only ever charged when the owner had no SOL and Strata paid instead,
1379    /// and never exceeds 1% of the deposit.
1380    pub network_cost_atoms: String,
1381    /// The session key named in the request, if any.
1382    pub session_public_key: Option<String>,
1383    /// `true` when this transaction also registers `session_public_key` with
1384    /// the default session policy (the deposit doubles as onboarding);
1385    /// `false` when the key was already registered or none was named.
1386    pub registers_session: bool,
1387    pub transaction_base64: String,
1388    pub recent_blockhash: String,
1389    pub owner_signature_required: bool,
1390    /// Opaque handle for this prepared transaction. Hand it back with the
1391    /// owner-signed transaction to `vault.relay` and Strata submits it — no
1392    /// RPC or SOL needed on the owner side.
1393    pub preparation_id: String,
1394    /// `true` when Strata is the transaction fee payer and covers any rent the
1395    /// action creates, so the owner needs no SOL at all. `false` means the
1396    /// owner wallet is the fee payer (Strata still submits it on request).
1397    pub sponsored: bool,
1398    /// The prepared transaction must be submitted before this server time.
1399    pub submit_by_ms: u64,
1400}
1401
1402#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1403#[serde(deny_unknown_fields)]
1404pub struct PlatformVaultWithdrawPrepareRequest {
1405    pub wallet_address: String,
1406    pub market_id: String,
1407    pub asset_id: String,
1408    pub destination_wallet_address: String,
1409    pub amount_atoms: String,
1410}
1411
1412/// Exact owner-authorized withdrawal transaction. The destination is a wallet
1413/// identity; account construction and private balance routing remain internal.
1414#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1415#[serde(deny_unknown_fields)]
1416pub struct PlatformVaultWithdrawPrepareResponse {
1417    pub schema_version: u16,
1418    pub contract_version: String,
1419    pub server_time_ms: u64,
1420    pub wallet_address: String,
1421    pub market_id: String,
1422    pub asset_id: String,
1423    pub destination_wallet_address: String,
1424    pub amount_atoms: String,
1425    pub transaction_base64: String,
1426    pub recent_blockhash: String,
1427    pub owner_signature_required: bool,
1428    /// Opaque handle for this prepared transaction. Hand it back with the
1429    /// owner-signed transaction to `vault.relay` and Strata submits it — no
1430    /// RPC or SOL needed on the owner side.
1431    pub preparation_id: String,
1432    /// `true` when Strata is the transaction fee payer and covers any rent the
1433    /// action creates, so the owner needs no SOL at all. `false` means the
1434    /// owner wallet is the fee payer (Strata still submits it on request).
1435    pub sponsored: bool,
1436    /// The prepared transaction must be submitted before this server time.
1437    pub submit_by_ms: u64,
1438}
1439
1440/// Which prepared Vault action a submission carries.
1441#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1442#[serde(rename_all = "snake_case")]
1443pub enum PlatformVaultAction {
1444    Setup,
1445    Deposit,
1446    Withdraw,
1447    Delegate,
1448    Policy,
1449    Pause,
1450}
1451
1452#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1453#[serde(rename_all = "snake_case")]
1454pub enum PlatformVaultSubmissionStatus {
1455    /// Accepted by the cluster; confirmation pending.
1456    Submitted,
1457    Confirmed,
1458    Failed,
1459}
1460
1461/// Submit an owner-signed prepared Vault transaction. Strata verifies it is
1462/// exactly the prepared transaction, adds its own fee-payer signature when
1463/// the preparation was sponsored, and broadcasts it. Idempotent per
1464/// `idempotency_key`.
1465#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1466#[serde(deny_unknown_fields)]
1467pub struct PlatformVaultSubmitRequest {
1468    pub preparation_id: String,
1469    pub signed_transaction_base64: String,
1470    pub idempotency_key: String,
1471}
1472
1473/// Durable outcome of a Vault submission, also returned by the status read.
1474#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1475#[serde(deny_unknown_fields)]
1476pub struct PlatformVaultSubmitResponse {
1477    pub schema_version: u16,
1478    pub contract_version: String,
1479    pub preparation_id: String,
1480    pub action: PlatformVaultAction,
1481    pub wallet_address: String,
1482    pub sponsored: bool,
1483    pub signature: String,
1484    pub status: PlatformVaultSubmissionStatus,
1485    /// Present only when `status` is `failed`.
1486    pub failure_code: Option<String>,
1487    pub updated_at_ms: u64,
1488}
1489
1490#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1491#[serde(deny_unknown_fields)]
1492pub struct PlatformRewardStanding {
1493    pub rank: u32,
1494    pub wallet_address: String,
1495    pub points: String,
1496}
1497
1498#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1499#[serde(deny_unknown_fields)]
1500pub struct PlatformOwnerRewards {
1501    pub wallet_address: String,
1502    pub rank: Option<u32>,
1503    pub points: String,
1504    pub trading_points: String,
1505    pub making_points: String,
1506    pub bug_points: String,
1507    pub referral_points: String,
1508}
1509
1510#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1511#[serde(deny_unknown_fields)]
1512pub struct PlatformRewardsResponse {
1513    pub schema_version: u16,
1514    pub contract_version: String,
1515    pub server_time_ms: u64,
1516    pub season: String,
1517    pub total_wallets: u32,
1518    pub owner: Option<PlatformOwnerRewards>,
1519    pub standings: Vec<PlatformRewardStanding>,
1520}
1521
1522#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1523#[serde(deny_unknown_fields)]
1524pub struct PlatformReferralsResponse {
1525    pub schema_version: u16,
1526    pub contract_version: String,
1527    pub server_time_ms: u64,
1528    pub wallet_address: String,
1529    pub enabled: bool,
1530    pub cash_rewards_enabled: bool,
1531    pub referral_code: Option<String>,
1532    pub referred_wallets: u32,
1533    pub referral_points: String,
1534    pub referred_by: Option<String>,
1535    pub referral_locked: bool,
1536    pub cash_accrued_atoms: String,
1537    pub cash_paid_atoms: String,
1538    pub cash_claimable_atoms: String,
1539}
1540
1541#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1542#[serde(deny_unknown_fields)]
1543pub struct PlatformReferralLinkRequest {
1544    pub wallet_address: String,
1545    pub referral_code: String,
1546    pub authorization_signature: String,
1547}
1548
1549#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1550#[serde(deny_unknown_fields)]
1551pub struct PlatformReferralLinkResponse {
1552    pub schema_version: u16,
1553    pub contract_version: String,
1554    pub server_time_ms: u64,
1555    pub wallet_address: String,
1556    pub referral_code: String,
1557    pub status: String,
1558}
1559
1560#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1561#[serde(deny_unknown_fields)]
1562pub struct PlatformReferralClaimRequest {
1563    pub wallet_address: String,
1564    pub payout_wallet_address: Option<String>,
1565    pub authorization_signature: String,
1566}
1567
1568#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1569#[serde(deny_unknown_fields)]
1570pub struct PlatformReferralClaimResponse {
1571    pub schema_version: u16,
1572    pub contract_version: String,
1573    pub server_time_ms: u64,
1574    pub wallet_address: String,
1575    pub payout_wallet_address: String,
1576    pub claimable_atoms: String,
1577    pub status: String,
1578}
1579
1580#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1581#[serde(rename_all = "snake_case")]
1582pub enum PlatformBugStatus {
1583    Pending,
1584    Confirmed,
1585    Rejected,
1586}
1587
1588#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1589#[serde(deny_unknown_fields)]
1590pub struct PlatformBugReport {
1591    pub bug_id: String,
1592    pub status: PlatformBugStatus,
1593    pub severity: u8,
1594    pub points: String,
1595    pub created_at_ms: u64,
1596    pub triaged_at_ms: Option<u64>,
1597    pub completed_at_ms: Option<u64>,
1598}
1599
1600#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1601#[serde(deny_unknown_fields)]
1602pub struct PlatformBugsResponse {
1603    pub schema_version: u16,
1604    pub contract_version: String,
1605    pub server_time_ms: u64,
1606    pub wallet_address: String,
1607    pub points: String,
1608    pub confirmed_reports: u32,
1609    pub reports: Vec<PlatformBugReport>,
1610}
1611
1612#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1613#[serde(deny_unknown_fields)]
1614pub struct PlatformBugSubmitRequest {
1615    pub owner_wallet: String,
1616    pub message: String,
1617    /// Hex Ed25519 signature over `strata-bug-report:v1:` followed by the
1618    /// trimmed report message. Signing always happens outside Strata.
1619    pub authorization_signature: String,
1620}
1621
1622#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1623#[serde(deny_unknown_fields)]
1624pub struct PlatformBugSubmitResponse {
1625    pub schema_version: u16,
1626    pub contract_version: String,
1627    pub server_time_ms: u64,
1628    pub bug_id: String,
1629    pub status: PlatformBugStatus,
1630}
1631
1632#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1633#[serde(rename_all = "snake_case")]
1634pub enum PlatformTradeSide {
1635    Buy,
1636    Sell,
1637}
1638
1639#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1640#[serde(deny_unknown_fields)]
1641pub struct PlatformTrade {
1642    pub trade_id: String,
1643    pub side: PlatformTradeSide,
1644    pub price_atoms: String,
1645    pub size_atoms: String,
1646    pub executed_at_ms: u64,
1647}
1648
1649#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1650#[serde(deny_unknown_fields)]
1651pub struct PlatformTradesResponse {
1652    pub schema_version: u16,
1653    pub contract_version: String,
1654    pub market_id: String,
1655    pub server_time_ms: u64,
1656    pub trades: Vec<PlatformTrade>,
1657}
1658
1659#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1660#[serde(rename_all = "snake_case")]
1661pub enum PlatformOrderType {
1662    GoodUntilCancelled,
1663    ImmediateOrCancel,
1664    FillOrKill,
1665    PostOnly,
1666}
1667
1668/// Externally authorized resting-order operation. The public contract exposes
1669/// product intent only; private construction details never cross the SDK
1670/// boundary.
1671#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1672#[serde(rename_all = "snake_case")]
1673pub enum PlatformOrderAction {
1674    Place,
1675    Cancel,
1676    CancelAll,
1677    /// Atomically cancel one existing order and place its explicitly bound
1678    /// successor in the same transaction.
1679    Replace,
1680    /// Atomically execute a bounded heterogeneous set of place, cancel, and
1681    /// replace operations in one transaction.
1682    Batch,
1683}
1684
1685/// One operation inside an atomic order-control batch. Owner and session
1686/// identity live on the enclosing challenge so no item can widen authority.
1687#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1688#[serde(tag = "action", rename_all = "snake_case", deny_unknown_fields)]
1689pub enum PlatformOrderBatchOperation {
1690    Place {
1691        /// Vault market account sequence for this order. Omit it and Strata
1692        /// resolves the next sequence from the Vault's confirmed market
1693        /// account when the challenge is issued (consecutive places in one
1694        /// batch receive consecutive sequences); supply it to pin a sequence
1695        /// tracked locally. A batch must either supply every sequence or none.
1696        #[serde(default, skip_serializing_if = "Option::is_none")]
1697        account_sequence: Option<String>,
1698        client_order_id: String,
1699        side: PlatformTradeSide,
1700        order_type: PlatformOrderType,
1701        limit_price_atoms: String,
1702        size_atoms: String,
1703    },
1704    Cancel {
1705        order_id: String,
1706    },
1707    Replace {
1708        order_id: String,
1709        #[serde(default, skip_serializing_if = "Option::is_none")]
1710        account_sequence: Option<String>,
1711        client_order_id: String,
1712        side: PlatformTradeSide,
1713        order_type: PlatformOrderType,
1714        limit_price_atoms: String,
1715        size_atoms: String,
1716    },
1717}
1718
1719/// Request canonical bytes for one externally signed order-control operation.
1720/// Variant-specific fields are sealed so an authorization cannot be widened
1721/// between challenge and transaction preparation.
1722#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1723#[serde(tag = "action", rename_all = "snake_case", deny_unknown_fields)]
1724pub enum PlatformOrderChallengeRequest {
1725    Place {
1726        owner_wallet: String,
1727        session_public_key: String,
1728        /// Vault market account sequence. Omit it and Strata resolves the next
1729        /// sequence from the Vault's confirmed market account when the
1730        /// challenge is issued; supply it to pin a sequence tracked locally.
1731        #[serde(default, skip_serializing_if = "Option::is_none")]
1732        account_sequence: Option<String>,
1733        client_order_id: String,
1734        side: PlatformTradeSide,
1735        order_type: PlatformOrderType,
1736        limit_price_atoms: String,
1737        size_atoms: String,
1738    },
1739    Cancel {
1740        owner_wallet: String,
1741        session_public_key: String,
1742        order_id: String,
1743    },
1744    CancelAll {
1745        owner_wallet: String,
1746        session_public_key: String,
1747    },
1748    Replace {
1749        owner_wallet: String,
1750        session_public_key: String,
1751        order_id: String,
1752        #[serde(default, skip_serializing_if = "Option::is_none")]
1753        account_sequence: Option<String>,
1754        client_order_id: String,
1755        side: PlatformTradeSide,
1756        order_type: PlatformOrderType,
1757        limit_price_atoms: String,
1758        size_atoms: String,
1759    },
1760    Batch {
1761        owner_wallet: String,
1762        session_public_key: String,
1763        operations: Vec<PlatformOrderBatchOperation>,
1764    },
1765}
1766
1767#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1768#[serde(deny_unknown_fields)]
1769pub struct PlatformOrderChallengeResponse {
1770    pub schema_version: u16,
1771    pub contract_version: String,
1772    pub challenge_id: String,
1773    pub market_id: String,
1774    pub action: PlatformOrderAction,
1775    /// Exact opaque order set bound by the authorization. Replace returns the
1776    /// old then new ID. Batch flattens item IDs in request order, with replace
1777    /// contributing old then new. A batch contains at most six operations.
1778    pub order_ids: Vec<String>,
1779    pub authorization_payload_base64: String,
1780    pub server_time_ms: u64,
1781    pub expires_at_ms: u64,
1782}
1783
1784/// A prepared challenge, signed: the two-step path.
1785#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1786#[serde(deny_unknown_fields)]
1787pub struct PlatformOrderPrepareAuthorization {
1788    pub challenge_id: String,
1789    /// Base58 Ed25519 signature over `authorization_payload_base64`. Required
1790    /// over HTTP. Over the session-authenticated order command channel it may
1791    /// be omitted: the socket already proved the session and the challenge is
1792    /// bound to it, so the session signs only the transaction (one signature).
1793    #[serde(default, skip_serializing_if = "Option::is_none")]
1794    pub authorization_signature: Option<String>,
1795}
1796
1797/// Prepare an order-control transaction. Either hand back a signed challenge
1798/// (`Authorized`, two signatures per action) or send the operation itself
1799/// (`Direct`, one signature per action): Strata builds the transaction from
1800/// the operation immediately and the session's signature over that
1801/// transaction is the whole authorization. The response is identical.
1802#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1803#[serde(untagged)]
1804pub enum PlatformOrderPrepareRequest {
1805    Authorized(PlatformOrderPrepareAuthorization),
1806    Direct(PlatformOrderChallengeRequest),
1807}
1808
1809#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1810#[serde(deny_unknown_fields)]
1811pub struct PlatformOrderPrepareResponse {
1812    pub schema_version: u16,
1813    pub contract_version: String,
1814    pub order_control_id: String,
1815    pub market_id: String,
1816    pub action: PlatformOrderAction,
1817    pub order_ids: Vec<String>,
1818    /// Backend-partially-signed Solana v0 transaction. The external session
1819    /// signer verifies and fills only its signature slot.
1820    pub transaction_base64: String,
1821    pub recent_blockhash: String,
1822    pub last_valid_block_height: u64,
1823    pub expires_at_ms: u64,
1824}
1825
1826#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1827#[serde(deny_unknown_fields)]
1828pub struct PlatformOrderSubmitRequest {
1829    pub order_control_id: String,
1830    pub signed_transaction_base64: String,
1831    pub idempotency_key: String,
1832}
1833
1834#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1835#[serde(rename_all = "snake_case")]
1836pub enum PlatformOrderSubmissionStatus {
1837    Submitted,
1838}
1839
1840#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1841#[serde(deny_unknown_fields)]
1842pub struct PlatformOrderSubmitResponse {
1843    pub schema_version: u16,
1844    pub contract_version: String,
1845    pub order_control_id: String,
1846    pub market_id: String,
1847    pub action: PlatformOrderAction,
1848    pub order_ids: Vec<String>,
1849    pub signature: String,
1850    pub status: PlatformOrderSubmissionStatus,
1851}
1852
1853#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1854#[serde(deny_unknown_fields)]
1855pub struct PlatformOrderStatusRequest {
1856    pub order_control_id: String,
1857    pub idempotency_key: String,
1858}
1859
1860#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1861#[serde(rename_all = "snake_case")]
1862pub enum PlatformOrderControlStatus {
1863    Submitting,
1864    Submitted,
1865    Failed,
1866}
1867
1868#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1869#[serde(deny_unknown_fields)]
1870pub struct PlatformOrderStatusResponse {
1871    pub schema_version: u16,
1872    pub contract_version: String,
1873    pub order_control_id: String,
1874    pub market_id: String,
1875    pub action: PlatformOrderAction,
1876    pub order_ids: Vec<String>,
1877    pub signature: String,
1878    pub status: PlatformOrderControlStatus,
1879    pub failure_code: Option<String>,
1880    pub updated_at_ms: u64,
1881}
1882
1883/// Collision policy for an incoming order that would cross the owner's own
1884/// resting liquidity. Every mode still preserves Strata's matcher and on-chain
1885/// self-fill prohibition; this only controls which order is cancelled first.
1886#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1887#[serde(rename_all = "snake_case")]
1888pub enum PlatformSelfTradePrevention {
1889    CancelTaker,
1890    CancelMaker,
1891    CancelBoth,
1892    SkipOwnLiquidity,
1893}
1894
1895/// One command on the persistent order-control connection. Challenge results
1896/// may contain an effective request that differs from the requested one only
1897/// by the explicitly selected self-trade prevention transformation.
1898#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1899#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
1900pub enum PlatformOrderCommand {
1901    /// Authenticated non-trading round trip used for latency certification.
1902    Probe {
1903        nonce: String,
1904    },
1905    Challenge {
1906        request: PlatformOrderChallengeRequest,
1907        self_trade_prevention: PlatformSelfTradePrevention,
1908    },
1909    Prepare {
1910        request: PlatformOrderPrepareRequest,
1911    },
1912    Submit {
1913        request: PlatformOrderSubmitRequest,
1914    },
1915    Status {
1916        request: PlatformOrderStatusRequest,
1917    },
1918    DeadManArm {
1919        timeout_ms: u64,
1920        request: PlatformOrderSubmitRequest,
1921    },
1922    DeadManStatus,
1923    DeadManHeartbeat,
1924    DeadManDisarm,
1925}
1926
1927/// Frames sent by an external agent. Authentication proves possession of the
1928/// declared session key; individual order authorizations and transactions keep
1929/// their existing exact external-signing boundaries. Authentication is a
1930/// singleton frame. After authentication, the transport accepts either one
1931/// command or a bounded array of commands; every command retains its own
1932/// request ID and contiguous sequence.
1933#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1934#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
1935pub enum PlatformOrderCommandClientFrame {
1936    Authenticate {
1937        owner_wallet: String,
1938        session_public_key: String,
1939        /// Base58 Ed25519 signature over the stream authentication payload.
1940        signature: String,
1941        /// Optional negotiated result framing. Omitted clients retain the
1942        /// complete-event array format.
1943        #[serde(default, skip_serializing_if = "Option::is_none")]
1944        batch_format: Option<PlatformOrderCommandBatchFormat>,
1945    },
1946    Command {
1947        request_id: String,
1948        sequence: String,
1949        command: PlatformOrderCommand,
1950    },
1951}
1952
1953#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1954#[serde(rename_all = "snake_case")]
1955pub enum PlatformOrderCommandBatchFormat {
1956    CompactV1,
1957}
1958
1959/// One result inside a compact event batch. Shared stream identity, time and
1960/// sequence metadata live on the enclosing frame; request correlation and
1961/// command-specific results remain independent.
1962#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1963#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
1964pub enum PlatformOrderCommandBatchEvent {
1965    ProbeResult {
1966        request_id: String,
1967        nonce: String,
1968    },
1969    ChallengeResult {
1970        request_id: String,
1971        self_trade_prevention: PlatformSelfTradePrevention,
1972        prevented_order_ids: Vec<String>,
1973        effective_request: PlatformOrderChallengeRequest,
1974        response: PlatformOrderChallengeResponse,
1975    },
1976    PrepareResult {
1977        request_id: String,
1978        response: PlatformOrderPrepareResponse,
1979    },
1980    SubmitResult {
1981        request_id: String,
1982        response: PlatformOrderSubmitResponse,
1983    },
1984    StatusResult {
1985        request_id: String,
1986        response: PlatformOrderStatusResponse,
1987    },
1988    DeadManResult {
1989        request_id: String,
1990        state: PlatformDeadManState,
1991    },
1992    CommandError {
1993        request_id: String,
1994        error: PublicOperationError,
1995    },
1996    Heartbeat,
1997}
1998
1999#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2000#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
2001pub enum PlatformOrderCommandServerFrame {
2002    EventBatch {
2003        schema_version: u16,
2004        contract_version: String,
2005        market_id: String,
2006        stream_id: String,
2007        first_sequence: String,
2008        previous_sequence: String,
2009        server_time_ms: u64,
2010        events: Vec<PlatformOrderCommandBatchEvent>,
2011    },
2012}
2013
2014#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
2015#[serde(rename_all = "snake_case")]
2016pub enum PlatformDeadManStatus {
2017    Armed,
2018    Triggering,
2019    Triggered,
2020    Disarmed,
2021    Expired,
2022    Failed,
2023}
2024
2025#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2026#[serde(deny_unknown_fields)]
2027pub struct PlatformDeadManState {
2028    pub status: PlatformDeadManStatus,
2029    pub timeout_ms: u64,
2030    pub heartbeat_deadline_ms: u64,
2031    pub order_control_id: Option<String>,
2032    pub signature: Option<String>,
2033    pub failure_code: Option<String>,
2034    pub updated_at_ms: u64,
2035}
2036
2037/// One sequenced event emitted by the persistent order-control connection.
2038/// After authentication, the transport carries bounded arrays of these events
2039/// so concurrent results share frame overhead without weakening per-event
2040/// sequence or request correlation. Terminal chain status may arrive later
2041/// without blocking command submission.
2042#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2043#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
2044pub enum PlatformOrderCommandEvent {
2045    AuthChallenge {
2046        schema_version: u16,
2047        contract_version: String,
2048        market_id: String,
2049        challenge: String,
2050        server_time_ms: u64,
2051        expires_at_ms: u64,
2052    },
2053    Ready {
2054        schema_version: u16,
2055        contract_version: String,
2056        market_id: String,
2057        stream_id: String,
2058        sequence: String,
2059        server_time_ms: u64,
2060    },
2061    ProbeResult {
2062        schema_version: u16,
2063        contract_version: String,
2064        market_id: String,
2065        stream_id: String,
2066        sequence: String,
2067        previous_sequence: String,
2068        request_id: String,
2069        nonce: String,
2070        server_time_ms: u64,
2071    },
2072    ChallengeResult {
2073        schema_version: u16,
2074        contract_version: String,
2075        market_id: String,
2076        stream_id: String,
2077        sequence: String,
2078        previous_sequence: String,
2079        request_id: String,
2080        self_trade_prevention: PlatformSelfTradePrevention,
2081        prevented_order_ids: Vec<String>,
2082        effective_request: PlatformOrderChallengeRequest,
2083        response: PlatformOrderChallengeResponse,
2084        server_time_ms: u64,
2085    },
2086    PrepareResult {
2087        schema_version: u16,
2088        contract_version: String,
2089        market_id: String,
2090        stream_id: String,
2091        sequence: String,
2092        previous_sequence: String,
2093        request_id: String,
2094        response: PlatformOrderPrepareResponse,
2095        server_time_ms: u64,
2096    },
2097    SubmitResult {
2098        schema_version: u16,
2099        contract_version: String,
2100        market_id: String,
2101        stream_id: String,
2102        sequence: String,
2103        previous_sequence: String,
2104        request_id: String,
2105        response: PlatformOrderSubmitResponse,
2106        server_time_ms: u64,
2107    },
2108    StatusResult {
2109        schema_version: u16,
2110        contract_version: String,
2111        market_id: String,
2112        stream_id: String,
2113        sequence: String,
2114        previous_sequence: String,
2115        request_id: String,
2116        response: PlatformOrderStatusResponse,
2117        server_time_ms: u64,
2118    },
2119    DeadManResult {
2120        schema_version: u16,
2121        contract_version: String,
2122        market_id: String,
2123        stream_id: String,
2124        sequence: String,
2125        previous_sequence: String,
2126        request_id: String,
2127        state: PlatformDeadManState,
2128        server_time_ms: u64,
2129    },
2130    CommandError {
2131        schema_version: u16,
2132        contract_version: String,
2133        market_id: String,
2134        stream_id: String,
2135        sequence: String,
2136        previous_sequence: String,
2137        request_id: String,
2138        error: PublicOperationError,
2139        server_time_ms: u64,
2140    },
2141    Heartbeat {
2142        schema_version: u16,
2143        contract_version: String,
2144        market_id: String,
2145        stream_id: String,
2146        sequence: String,
2147        previous_sequence: String,
2148        server_time_ms: u64,
2149    },
2150}
2151
2152#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2153#[serde(deny_unknown_fields)]
2154pub struct PlatformAccountOrder {
2155    pub order_id: String,
2156    pub side: PlatformTradeSide,
2157    pub order_type: PlatformOrderType,
2158    pub state: PlatformOrderState,
2159    pub limit_price_atoms: String,
2160    pub original_size_atoms: String,
2161    pub remaining_size_atoms: String,
2162}
2163
2164#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2165#[serde(deny_unknown_fields)]
2166pub struct PlatformAccountFill {
2167    pub fill_id: String,
2168    pub side: PlatformTradeSide,
2169    pub price_atoms: String,
2170    pub size_atoms: String,
2171    pub fee_quote_atoms: String,
2172    pub fee_is_final: bool,
2173    pub settlement: PlatformSettlementState,
2174    pub executed_at_ms: u64,
2175    pub confirmed_at_ms: Option<u64>,
2176    pub transaction_id: Option<String>,
2177    pub realized_pnl_quote_atoms: String,
2178}
2179
2180#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2181#[serde(deny_unknown_fields)]
2182pub struct PlatformAccountSnapshotResponse {
2183    pub schema_version: u16,
2184    pub contract_version: String,
2185    pub market_id: String,
2186    pub wallet_address: String,
2187    pub server_time_ms: u64,
2188    pub orders: Vec<PlatformAccountOrder>,
2189    pub fills: Vec<PlatformAccountFill>,
2190}
2191
2192#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
2193#[serde(rename_all = "snake_case")]
2194pub enum PlatformMakerReputationTier {
2195    Probation,
2196    Bronze,
2197    Silver,
2198    Gold,
2199    Platinum,
2200}
2201
2202#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2203#[serde(deny_unknown_fields)]
2204pub struct PlatformMakerTierProgress {
2205    pub next_tier: Option<PlatformMakerReputationTier>,
2206    pub reputation_score_required: Option<u16>,
2207    pub reputation_score_remaining: u16,
2208    pub quote_requests_required: Option<String>,
2209    pub quote_requests_remaining: String,
2210    pub stake_atoms_required: Option<String>,
2211    pub stake_atoms_remaining: String,
2212    pub tenure_slots_required: Option<String>,
2213    pub tenure_slots_remaining: String,
2214}
2215
2216/// Authenticated, privacy-preserving reliability and participation record for the
2217/// requesting maker. All potentially large counters and atomic quantities are
2218/// decimal strings so JavaScript agents never lose integer precision.
2219#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2220#[serde(deny_unknown_fields)]
2221pub struct PlatformMakerReputationResponse {
2222    pub schema_version: u16,
2223    pub contract_version: String,
2224    pub market_id: String,
2225    pub maker_id: String,
2226    pub wallet_address: String,
2227    pub active: bool,
2228    pub tier: PlatformMakerReputationTier,
2229    pub reputation_score: u16,
2230    pub total_quote_requests: String,
2231    pub successful_fills: String,
2232    pub missed_quote_requests: String,
2233    pub fill_rate_bps: u16,
2234    pub consecutive_misses: u16,
2235    pub lifetime_filled_quote_atoms: String,
2236    pub distinct_counterparties: u16,
2237    pub recent_average_latency_ms: u16,
2238    pub configured_minimum_spread_bps: u16,
2239    pub weighted_average_spread_bps: u16,
2240    pub stake_atoms: String,
2241    pub epoch_start_stake_atoms: String,
2242    pub epoch_slashed_atoms: String,
2243    pub epoch_slashed_bps: u16,
2244    pub lifetime_auto_slashed_atoms: String,
2245    pub registered_slot: String,
2246    pub last_active_slot: String,
2247    pub last_settled_slot: String,
2248    pub revoked_at_slot: Option<String>,
2249    pub tenure_slots: String,
2250    pub signed_quote_stream_eligible: bool,
2251    pub minimum_quote_interval_ms: Option<u16>,
2252    pub tier_progress: PlatformMakerTierProgress,
2253    pub server_time_ms: u64,
2254}
2255
2256#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
2257#[serde(rename_all = "snake_case")]
2258pub enum PlatformMakerSide {
2259    Buy,
2260    Sell,
2261}
2262
2263#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
2264#[serde(rename_all = "snake_case")]
2265pub enum PlatformOracleHealth {
2266    Fresh,
2267    Stale,
2268    Unknown,
2269}
2270
2271/// The maker's resting firm orders in this market, summarised by side.
2272#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2273#[serde(deny_unknown_fields)]
2274pub struct PlatformMakerFirmOrderSummary {
2275    pub resting_orders: u32,
2276    pub bid_orders: u32,
2277    pub ask_orders: u32,
2278    pub bid_size_atoms: String,
2279    pub ask_size_atoms: String,
2280}
2281
2282/// One of the maker's own live signed quotes in the streaming lane.
2283#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2284#[serde(deny_unknown_fields)]
2285pub struct PlatformMakerSignedQuote {
2286    pub side: PlatformMakerSide,
2287    pub price_atoms: String,
2288    pub size_atoms: String,
2289    pub nonce: String,
2290    pub issued_at_ms: u64,
2291    pub expires_at_ms: u64,
2292}
2293
2294/// The maker's own intent product in this market.
2295#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2296#[serde(deny_unknown_fields)]
2297pub struct PlatformMakerIntentStatus {
2298    pub active: bool,
2299    pub side: PlatformMakerSide,
2300    pub minimum_price_atoms: String,
2301    pub maximum_price_atoms: String,
2302    pub maximum_fill_size_atoms: String,
2303    /// Fill budget still available after in-flight reservations.
2304    pub remaining_fill_size_atoms: String,
2305    pub minimum_spread_bps: u16,
2306    pub stake_atoms: String,
2307}
2308
2309/// The maker's signed-quote lane: eligibility and its own live quotes.
2310#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2311#[serde(deny_unknown_fields)]
2312pub struct PlatformMakerSignedQuoteLane {
2313    pub eligible: bool,
2314    pub live_quotes: Vec<PlatformMakerSignedQuote>,
2315}
2316
2317#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2318#[serde(deny_unknown_fields)]
2319pub struct PlatformMakerStrandLevel {
2320    /// Null when the configured offset overflows the price range.
2321    pub price_atoms: Option<String>,
2322    pub size_atoms: String,
2323    pub remaining_size_atoms: String,
2324}
2325
2326/// One of the maker's own Strands in this market.
2327#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2328#[serde(deny_unknown_fields)]
2329pub struct PlatformMakerStrandStatus {
2330    pub enabled: bool,
2331    pub async_only: bool,
2332    /// True once the chain would reject fills because `valid_until_slot` passed.
2333    pub expired: bool,
2334    pub mid_price_atoms: String,
2335    pub tick_size_atoms: String,
2336    /// Null means the Strand never expires.
2337    pub valid_until_slot: Option<String>,
2338    pub bids: Vec<PlatformMakerStrandLevel>,
2339    pub asks: Vec<PlatformMakerStrandLevel>,
2340    pub maximum_exposure_atoms: String,
2341    pub remaining_exposure_atoms: String,
2342}
2343
2344/// One of the maker's own Currents in this market.
2345#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2346#[serde(deny_unknown_fields)]
2347pub struct PlatformMakerCurrentStatus {
2348    pub enabled: bool,
2349    pub async_only: bool,
2350    pub expired: bool,
2351    pub half_spread_bps: u16,
2352    pub band_step_bps: u16,
2353    pub maximum_confidence_bps: u16,
2354    pub maximum_oracle_age_seconds: u32,
2355    pub sync_spread_bps: u16,
2356    /// Null means the Current never expires.
2357    pub valid_until_slot: Option<String>,
2358    pub bid_depth_atoms: Vec<String>,
2359    pub ask_depth_atoms: Vec<String>,
2360    pub maximum_exposure_atoms: String,
2361    pub remaining_exposure_atoms: String,
2362    /// Freshness class of the live Strata mark used to price this Current.
2363    pub oracle_health: PlatformOracleHealth,
2364}
2365
2366/// One durable dead-man guard the owner armed for a session in this market.
2367#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2368#[serde(deny_unknown_fields)]
2369pub struct PlatformMakerDeadManGuard {
2370    pub session_public_key: String,
2371    pub status: PlatformDeadManStatus,
2372    pub timeout_ms: u64,
2373    pub heartbeat_deadline_ms: u64,
2374    pub updated_at_ms: u64,
2375}
2376
2377/// Authenticated, owner-scoped view of the maker's Strata products in one
2378/// market: firm orders, intent, Strands, Currents, the signed-quote lane, live
2379/// exposure, health, and kill state. Nothing about other makers, takers, or
2380/// liquidity sources crosses this boundary.
2381#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2382#[serde(deny_unknown_fields)]
2383pub struct PlatformMakerStatusResponse {
2384    pub schema_version: u16,
2385    pub contract_version: String,
2386    pub market_id: String,
2387    pub maker_id: String,
2388    pub wallet_address: String,
2389    pub server_time_ms: u64,
2390    pub current_slot: String,
2391    pub firm_orders: PlatformMakerFirmOrderSummary,
2392    pub intent: Option<PlatformMakerIntentStatus>,
2393    pub signed_quotes: PlatformMakerSignedQuoteLane,
2394    pub strands: Vec<PlatformMakerStrandStatus>,
2395    pub currents: Vec<PlatformMakerCurrentStatus>,
2396    pub dead_man_guards: Vec<PlatformMakerDeadManGuard>,
2397    /// Count of maker products currently able to fill: an active intent, each
2398    /// enabled unexpired Strand or Current, and resting firm orders (as one).
2399    pub active_products: u16,
2400}
2401
2402/// One maker-owned Strand mutation. Amounts that may exceed JavaScript's safe
2403/// integer range remain canonical unsigned decimal strings on the wire.
2404#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2405#[serde(tag = "action", rename_all = "snake_case", deny_unknown_fields)]
2406pub enum PlatformMakerStrandPrepareRequest {
2407    Upsert {
2408        maker_wallet: String,
2409        enabled: bool,
2410        async_only: bool,
2411        sync_spread_ticks: u16,
2412        mid_price_atoms: String,
2413        #[serde(alias = "max_exposure_base_lots")]
2414        max_exposure_base_atoms: String,
2415        bid_offsets_ticks: Vec<u16>,
2416        ask_offsets_ticks: Vec<u16>,
2417        #[serde(alias = "bid_sizes_base_lots")]
2418        bid_sizes_base_atoms: Vec<String>,
2419        #[serde(alias = "ask_sizes_base_lots")]
2420        ask_sizes_base_atoms: Vec<String>,
2421        valid_until_slot: String,
2422    },
2423    Recenter {
2424        maker_wallet: String,
2425        new_mid_price_atoms: String,
2426        valid_until_slot: String,
2427    },
2428    SetEnabled {
2429        maker_wallet: String,
2430        enabled: bool,
2431    },
2432    Cancel {
2433        maker_wallet: String,
2434    },
2435}
2436
2437/// One maker-owned Current mutation. Current is parameterized around the
2438/// market's live Strata mark and therefore has no recenter action.
2439#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2440#[serde(tag = "action", rename_all = "snake_case", deny_unknown_fields)]
2441pub enum PlatformMakerCurrentPrepareRequest {
2442    Upsert {
2443        maker_wallet: String,
2444        enabled: bool,
2445        async_only: bool,
2446        half_spread_bps: u16,
2447        band_step_bps: u16,
2448        max_conf_bps: u16,
2449        max_oracle_dev_bps: u16,
2450        max_oracle_age_secs: u32,
2451        sync_spread_bps: u16,
2452        max_exposure_base_atoms: String,
2453        bid_depth_base_atoms: Vec<String>,
2454        ask_depth_base_atoms: Vec<String>,
2455        valid_until_slot: String,
2456    },
2457    Cancel {
2458        maker_wallet: String,
2459    },
2460}
2461
2462#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
2463#[serde(rename_all = "snake_case")]
2464pub enum PlatformMakerControlProduct {
2465    Strand,
2466    Current,
2467}
2468
2469#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
2470#[serde(rename_all = "snake_case")]
2471pub enum PlatformMakerControlAction {
2472    StrandUpsert,
2473    StrandRecenter,
2474    StrandSetEnabled,
2475    StrandCancel,
2476    CurrentUpsert,
2477    CurrentCancel,
2478}
2479
2480#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2481#[serde(deny_unknown_fields)]
2482pub struct PlatformMakerControlPrepareResponse {
2483    pub schema_version: u16,
2484    pub contract_version: String,
2485    pub maker_control_id: String,
2486    pub market_id: String,
2487    pub maker_wallet: String,
2488    pub product: PlatformMakerControlProduct,
2489    pub action: PlatformMakerControlAction,
2490    /// Unsigned legacy Solana transaction. The maker verifies the exact
2491    /// instruction and fills its only signature slot externally.
2492    pub transaction_base64: String,
2493    pub recent_blockhash: String,
2494    pub last_valid_block_height: u64,
2495    pub expires_at_ms: u64,
2496}
2497
2498#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2499#[serde(deny_unknown_fields)]
2500pub struct PlatformMakerControlSubmitRequest {
2501    pub maker_control_id: String,
2502    pub signed_transaction_base64: String,
2503    pub idempotency_key: String,
2504}
2505
2506#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
2507#[serde(rename_all = "snake_case")]
2508pub enum PlatformMakerControlSubmissionStatus {
2509    Submitted,
2510}
2511
2512#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2513#[serde(deny_unknown_fields)]
2514pub struct PlatformMakerControlSubmitResponse {
2515    pub schema_version: u16,
2516    pub contract_version: String,
2517    pub maker_control_id: String,
2518    pub market_id: String,
2519    pub maker_wallet: String,
2520    pub product: PlatformMakerControlProduct,
2521    pub action: PlatformMakerControlAction,
2522    pub signature: String,
2523    pub status: PlatformMakerControlSubmissionStatus,
2524}
2525
2526/// Which Strata maker product produced a maker-side fill.
2527#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
2528#[serde(rename_all = "snake_case")]
2529pub enum PlatformMakerProduct {
2530    FirmOrder,
2531    Intent,
2532    Strand,
2533    Current,
2534}
2535
2536/// One maker-side fill: the same sanitized settlement view as an account fill
2537/// plus the maker product that produced it. No counterparty or venue.
2538#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2539#[serde(deny_unknown_fields)]
2540pub struct PlatformMakerFill {
2541    pub fill_id: String,
2542    pub product: PlatformMakerProduct,
2543    pub side: PlatformTradeSide,
2544    pub price_atoms: String,
2545    pub size_atoms: String,
2546    pub fee_quote_atoms: String,
2547    pub fee_is_final: bool,
2548    pub settlement: PlatformSettlementState,
2549    pub executed_at_ms: u64,
2550    pub confirmed_at_ms: Option<u64>,
2551    pub transaction_id: Option<String>,
2552    pub realized_pnl_quote_atoms: String,
2553}
2554
2555/// Authenticated, sequenced owner-only maker stream (`mm.fills.stream`).
2556/// After the signed challenge the server sends one `maker_snapshot`, then
2557/// sequenced `maker_fill`, `maker_status` (exposure/product change), and
2558/// `heartbeat` events; a recovery snapshot advances the sequence on the same
2559/// stream identity.
2560#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2561#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
2562pub enum PlatformMakerEvent {
2563    AuthChallenge {
2564        schema_version: u16,
2565        contract_version: String,
2566        market_id: String,
2567        wallet_address: String,
2568        challenge: String,
2569        server_time_ms: u64,
2570        expires_at_ms: u64,
2571    },
2572    MakerSnapshot {
2573        schema_version: u16,
2574        contract_version: String,
2575        market_id: String,
2576        wallet_address: String,
2577        stream_id: String,
2578        sequence: String,
2579        server_time_ms: u64,
2580        status: PlatformMakerStatusResponse,
2581        fills: Vec<PlatformMakerFill>,
2582    },
2583    MakerFill {
2584        schema_version: u16,
2585        contract_version: String,
2586        market_id: String,
2587        wallet_address: String,
2588        stream_id: String,
2589        sequence: String,
2590        previous_sequence: String,
2591        server_time_ms: u64,
2592        fill: PlatformMakerFill,
2593    },
2594    MakerStatus {
2595        schema_version: u16,
2596        contract_version: String,
2597        market_id: String,
2598        wallet_address: String,
2599        stream_id: String,
2600        sequence: String,
2601        previous_sequence: String,
2602        server_time_ms: u64,
2603        status: PlatformMakerStatusResponse,
2604    },
2605    Heartbeat {
2606        schema_version: u16,
2607        contract_version: String,
2608        market_id: String,
2609        wallet_address: String,
2610        stream_id: String,
2611        sequence: String,
2612        previous_sequence: String,
2613        server_time_ms: u64,
2614    },
2615}
2616
2617#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2618#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
2619pub enum PlatformAccountEvent {
2620    AuthChallenge {
2621        schema_version: u16,
2622        contract_version: String,
2623        market_id: String,
2624        wallet_address: String,
2625        challenge: String,
2626        server_time_ms: u64,
2627        expires_at_ms: u64,
2628    },
2629    AccountSnapshot {
2630        schema_version: u16,
2631        contract_version: String,
2632        market_id: String,
2633        wallet_address: String,
2634        stream_id: String,
2635        sequence: String,
2636        server_time_ms: u64,
2637        orders: Vec<PlatformAccountOrder>,
2638        fills: Vec<PlatformAccountFill>,
2639    },
2640    OrdersSnapshot {
2641        schema_version: u16,
2642        contract_version: String,
2643        market_id: String,
2644        wallet_address: String,
2645        stream_id: String,
2646        sequence: String,
2647        previous_sequence: String,
2648        server_time_ms: u64,
2649        orders: Vec<PlatformAccountOrder>,
2650    },
2651    Fill {
2652        schema_version: u16,
2653        contract_version: String,
2654        market_id: String,
2655        wallet_address: String,
2656        stream_id: String,
2657        sequence: String,
2658        previous_sequence: String,
2659        server_time_ms: u64,
2660        fill: PlatformAccountFill,
2661    },
2662    Heartbeat {
2663        schema_version: u16,
2664        contract_version: String,
2665        market_id: String,
2666        wallet_address: String,
2667        stream_id: String,
2668        sequence: String,
2669        previous_sequence: String,
2670        server_time_ms: u64,
2671    },
2672}
2673
2674#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2675#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
2676pub enum PlatformMarketDataEvent {
2677    BookSnapshot {
2678        schema_version: u16,
2679        contract_version: String,
2680        market_id: String,
2681        stream_id: String,
2682        sequence: String,
2683        server_time_ms: u64,
2684        snapshot_id: String,
2685        bids: Vec<PlatformBookLevel>,
2686        asks: Vec<PlatformBookLevel>,
2687    },
2688    BookDelta {
2689        schema_version: u16,
2690        contract_version: String,
2691        market_id: String,
2692        stream_id: String,
2693        sequence: String,
2694        previous_sequence: String,
2695        server_time_ms: u64,
2696        changes: Vec<PlatformBookChange>,
2697    },
2698    BestBidAsk {
2699        schema_version: u16,
2700        contract_version: String,
2701        market_id: String,
2702        stream_id: String,
2703        sequence: String,
2704        server_time_ms: u64,
2705        best_bid: Option<PlatformBookLevel>,
2706        best_ask: Option<PlatformBookLevel>,
2707    },
2708    Trade {
2709        schema_version: u16,
2710        contract_version: String,
2711        market_id: String,
2712        server_time_ms: u64,
2713        trade: PlatformTrade,
2714    },
2715    MarketStatus {
2716        schema_version: u16,
2717        contract_version: String,
2718        market_id: String,
2719        server_time_ms: u64,
2720        status: PlatformMarketState,
2721    },
2722    Heartbeat {
2723        schema_version: u16,
2724        contract_version: String,
2725        market_id: String,
2726        server_time_ms: u64,
2727    },
2728}
2729
2730#[cfg(test)]
2731mod tests {
2732    use super::*;
2733
2734    #[test]
2735    fn public_platform_fixtures_decode_strictly() {
2736        let discovery: PlatformDiscoveryResponse =
2737            serde_json::from_str(PLATFORM_CAPABILITIES_FIXTURE).unwrap();
2738        let service_status: PlatformServiceStatusResponse =
2739            serde_json::from_str(PLATFORM_SERVICE_STATUS_FIXTURE).unwrap();
2740        let graph = PlatformActionGraphResponse::foundation();
2741        let assets: PlatformAssetsResponse = serde_json::from_str(PLATFORM_ASSETS_FIXTURE).unwrap();
2742        let swap_quote: PlatformSwapQuoteResponse =
2743            serde_json::from_str(PLATFORM_SWAP_QUOTE_FIXTURE).unwrap();
2744        let markets: PlatformMarketsResponse =
2745            serde_json::from_str(PLATFORM_MARKETS_FIXTURE).unwrap();
2746        let book: PlatformBookSnapshotResponse =
2747            serde_json::from_str(PLATFORM_BOOK_FIXTURE).unwrap();
2748        let bbo: PlatformBestBidAskResponse = serde_json::from_str(PLATFORM_BBO_FIXTURE).unwrap();
2749        let fees: PlatformFeeScheduleResponse =
2750            serde_json::from_str(PLATFORM_FEES_FIXTURE).unwrap();
2751        let status: PlatformMarketStatusResponse =
2752            serde_json::from_str(PLATFORM_STATUS_FIXTURE).unwrap();
2753        let candles: PlatformCandlesResponse =
2754            serde_json::from_str(PLATFORM_CANDLES_FIXTURE).unwrap();
2755        let mark: PlatformMarkResponse = serde_json::from_str(PLATFORM_MARK_FIXTURE).unwrap();
2756        let execution_status: PlatformExecutionStatusResponse =
2757            serde_json::from_str(PLATFORM_EXECUTION_STATUS_FIXTURE).unwrap();
2758        let twaps: PlatformTwapsResponse = serde_json::from_str(PLATFORM_TWAPS_FIXTURE).unwrap();
2759        let twap_challenge: PlatformTwapChallengeResponse =
2760            serde_json::from_str(PLATFORM_TWAP_CHALLENGE_FIXTURE).unwrap();
2761        let twap_prepare: PlatformTwapPrepareResponse =
2762            serde_json::from_str(PLATFORM_TWAP_PREPARE_FIXTURE).unwrap();
2763        let twap_submit: PlatformTwapSubmitResponse =
2764            serde_json::from_str(PLATFORM_TWAP_SUBMIT_FIXTURE).unwrap();
2765        let portfolio_history: PlatformPortfolioHistoryResponse =
2766            serde_json::from_str(PLATFORM_PORTFOLIO_HISTORY_FIXTURE).unwrap();
2767        let portfolio: PlatformPortfolioResponse =
2768            serde_json::from_str(PLATFORM_PORTFOLIO_FIXTURE).unwrap();
2769        let rewards: PlatformRewardsResponse =
2770            serde_json::from_str(PLATFORM_REWARDS_FIXTURE).unwrap();
2771        let referrals: PlatformReferralsResponse =
2772            serde_json::from_str(PLATFORM_REFERRALS_FIXTURE).unwrap();
2773        let referral_link: PlatformReferralLinkResponse =
2774            serde_json::from_str(PLATFORM_REFERRAL_LINK_FIXTURE).unwrap();
2775        let referral_claim: PlatformReferralClaimResponse =
2776            serde_json::from_str(PLATFORM_REFERRAL_CLAIM_FIXTURE).unwrap();
2777        let vault_status: PlatformVaultStatusResponse =
2778            serde_json::from_str(PLATFORM_VAULT_STATUS_FIXTURE).unwrap();
2779        let vault_pause: PlatformVaultPausePrepareResponse =
2780            serde_json::from_str(PLATFORM_VAULT_PAUSE_PREPARE_FIXTURE).unwrap();
2781        let vault_setup: PlatformVaultSetupPrepareResponse =
2782            serde_json::from_str(PLATFORM_VAULT_SETUP_PREPARE_FIXTURE).unwrap();
2783        let vault_delegate: PlatformVaultDelegatePrepareResponse =
2784            serde_json::from_str(PLATFORM_VAULT_DELEGATE_PREPARE_FIXTURE).unwrap();
2785        let vault_policy: PlatformVaultPolicyPrepareResponse =
2786            serde_json::from_str(PLATFORM_VAULT_POLICY_PREPARE_FIXTURE).unwrap();
2787        let vault_deposit: PlatformVaultDepositPrepareResponse =
2788            serde_json::from_str(PLATFORM_VAULT_DEPOSIT_PREPARE_FIXTURE).unwrap();
2789        let vault_withdraw: PlatformVaultWithdrawPrepareResponse =
2790            serde_json::from_str(PLATFORM_VAULT_WITHDRAW_PREPARE_FIXTURE).unwrap();
2791        let vault_submit: PlatformVaultSubmitResponse =
2792            serde_json::from_str(PLATFORM_VAULT_SUBMIT_FIXTURE).unwrap();
2793        let bugs: PlatformBugsResponse = serde_json::from_str(PLATFORM_BUGS_FIXTURE).unwrap();
2794        let bug_submit: PlatformBugSubmitResponse =
2795            serde_json::from_str(PLATFORM_BUG_SUBMIT_FIXTURE).unwrap();
2796        let trades: PlatformTradesResponse = serde_json::from_str(PLATFORM_TRADES_FIXTURE).unwrap();
2797        let account: PlatformAccountSnapshotResponse =
2798            serde_json::from_str(PLATFORM_ACCOUNT_FIXTURE).unwrap();
2799        let maker_reputation: PlatformMakerReputationResponse =
2800            serde_json::from_str(PLATFORM_MAKER_REPUTATION_FIXTURE).unwrap();
2801        let maker_status: PlatformMakerStatusResponse =
2802            serde_json::from_str(PLATFORM_MAKER_STATUS_FIXTURE).unwrap();
2803        let maker_stream: PlatformMakerEvent =
2804            serde_json::from_str(PLATFORM_MAKER_STREAM_FIXTURE).unwrap();
2805        let twap_stream: PlatformTwapEvent =
2806            serde_json::from_str(PLATFORM_TWAP_STREAM_FIXTURE).unwrap();
2807        let execution_stream: PlatformExecutionEvent =
2808            serde_json::from_str(PLATFORM_EXECUTION_STREAM_FIXTURE).unwrap();
2809        let order_challenge: PlatformOrderChallengeResponse =
2810            serde_json::from_str(PLATFORM_ORDER_CHALLENGE_FIXTURE).unwrap();
2811        let order_prepare: PlatformOrderPrepareResponse =
2812            serde_json::from_str(PLATFORM_ORDER_PREPARE_FIXTURE).unwrap();
2813        let order_submit: PlatformOrderSubmitResponse =
2814            serde_json::from_str(PLATFORM_ORDER_SUBMIT_FIXTURE).unwrap();
2815        let order_status: PlatformOrderStatusResponse =
2816            serde_json::from_str(PLATFORM_ORDER_STATUS_FIXTURE).unwrap();
2817
2818        assert_eq!(discovery.schema_version, PLATFORM_SCHEMA_VERSION);
2819        assert_eq!(service_status.status, PlatformServiceState::Operational);
2820        assert_eq!(service_status.available_operations, 59);
2821        assert_eq!(graph.entry_operation_id, "platform.capabilities.read");
2822        assert_eq!(graph.operations.len(), 69);
2823        assert_eq!(maker_reputation.tier, PlatformMakerReputationTier::Gold);
2824        assert_eq!(maker_status.active_products, 3);
2825        match &maker_stream {
2826            PlatformMakerEvent::MakerSnapshot { status, fills, .. } => {
2827                assert_eq!(status.active_products, maker_status.active_products);
2828                assert_eq!(fills.len(), 1);
2829                assert_eq!(fills[0].product, PlatformMakerProduct::Strand);
2830            }
2831            other => panic!("maker stream fixture must be a snapshot, got {other:?}"),
2832        }
2833        match &twap_stream {
2834            PlatformTwapEvent::TwapsSnapshot {
2835                twaps: streamed, ..
2836            } => {
2837                assert_eq!(streamed, &twaps.twaps);
2838            }
2839            other => panic!("twap stream fixture must be a snapshot, got {other:?}"),
2840        }
2841        match &execution_stream {
2842            PlatformExecutionEvent::ExecutionsSnapshot {
2843                executions,
2844                unknown_execution_ids,
2845                ..
2846            } => {
2847                assert_eq!(executions.len(), 2);
2848                assert_eq!(executions[0].execution_id, execution_status.execution_id);
2849                assert_eq!(unknown_execution_ids.len(), 1);
2850            }
2851            other => panic!("execution stream fixture must be a snapshot, got {other:?}"),
2852        }
2853        assert_eq!(maker_status.strands.len(), 1);
2854        assert_eq!(maker_status.currents.len(), 1);
2855        assert!(maker_status
2856            .intent
2857            .as_ref()
2858            .is_some_and(|intent| intent.active));
2859        assert_eq!(portfolio.balances.len(), 2);
2860        assert_eq!(portfolio.positions.len(), 1);
2861        assert!(portfolio.valuation_complete);
2862        assert_eq!(portfolio.equity_usd_micros.as_deref(), Some("439989500"));
2863        assert!(graph
2864            .operations
2865            .iter()
2866            .any(|operation| operation.id == "twap.place.submit"));
2867        assert!(graph
2868            .operations
2869            .iter()
2870            .any(|operation| operation.id == "twap.cancel.submit"));
2871        assert_eq!(discovery.capabilities.len(), 5);
2872        assert!(!discovery.authority.accepts_private_keys);
2873        assert_eq!(assets.assets.len(), 2);
2874        assert_eq!(swap_quote.input_asset_id, assets.assets[0].asset_id);
2875        assert_eq!(swap_quote.output_asset_id, assets.assets[1].asset_id);
2876        assert_eq!(markets.markets.len(), 1);
2877        assert_eq!(markets.markets[0].base_asset_id, assets.assets[0].asset_id);
2878        assert_eq!(markets.markets[0].quote_asset_id, assets.assets[1].asset_id);
2879        assert_eq!(book.sequence, "42");
2880        assert_eq!(bbo.best_bid.unwrap().price_atoms, "149990000");
2881        assert_eq!(fees.maximum_immediate_execution_fee_bps, 10);
2882        assert_eq!(status.status, PlatformMarketState::Active);
2883        assert_eq!(candles.candles.len(), 2);
2884        assert_eq!(mark.price_atoms_per_base_unit.as_deref(), Some("149995000"));
2885        assert_eq!(execution_status.status, PlatformExecutionState::Confirmed);
2886        assert_eq!(
2887            execution_status.settlement,
2888            PlatformSettlementState::Confirmed
2889        );
2890        assert_eq!(twaps.twaps[0].fills.len(), 1);
2891        assert_eq!(twaps.twaps[0].slices_executed, 2);
2892        assert_eq!(twap_challenge.action, PlatformTwapControlAction::Place);
2893        assert_eq!(twap_prepare.twap_id, twap_challenge.twap_id);
2894        assert_eq!(twap_submit.twap_control_id, twap_prepare.twap_control_id);
2895        assert_eq!(portfolio_history.points.len(), 2);
2896        assert_eq!(rewards.standings.len(), 2);
2897        assert!(referrals.enabled);
2898        assert_eq!(referral_link.status, "pending_first_fill");
2899        assert_eq!(referral_claim.status, "requested");
2900        assert_eq!(vault_status.state, PlatformVaultState::Active);
2901        assert_eq!(
2902            vault_status.session.as_ref().unwrap().state,
2903            PlatformVaultSessionState::Active
2904        );
2905        assert!(vault_pause.paused);
2906        assert!(vault_pause.owner_signature_required);
2907        assert_eq!(vault_setup.mode, PlatformVaultSetupMode::Create);
2908        assert!(vault_setup.permanent);
2909        assert_eq!(vault_delegate.action, PlatformVaultDelegateAction::Revoke);
2910        assert!(vault_delegate.owner_signature_required);
2911        assert_eq!(
2912            vault_policy.withdrawal_access.mode,
2913            PlatformVaultWithdrawalMode::Restricted
2914        );
2915        assert!(vault_policy.owner_signature_required);
2916        assert_eq!(vault_deposit.amount_atoms, "10000000");
2917        assert!(vault_deposit.owner_signature_required);
2918        assert_eq!(vault_withdraw.amount_atoms, "5000000");
2919        assert!(vault_withdraw.owner_signature_required);
2920        assert!(vault_withdraw.sponsored);
2921        assert!(vault_withdraw.preparation_id.starts_with("vp_"));
2922        assert_eq!(vault_submit.action, PlatformVaultAction::Deposit);
2923        assert_eq!(
2924            vault_submit.status,
2925            PlatformVaultSubmissionStatus::Submitted
2926        );
2927        assert!(vault_submit.sponsored);
2928        assert_eq!(vault_submit.failure_code, None);
2929        assert_eq!(bugs.reports[0].status, PlatformBugStatus::Confirmed);
2930        assert_eq!(bug_submit.status, PlatformBugStatus::Pending);
2931        assert_eq!(trades.trades.len(), 1);
2932        assert_eq!(account.orders.len(), 1);
2933        assert_eq!(account.fills.len(), 1);
2934        assert_eq!(order_challenge.action, PlatformOrderAction::Place);
2935        assert_eq!(order_prepare.order_ids, order_challenge.order_ids);
2936        assert_eq!(order_submit.order_ids, order_challenge.order_ids);
2937        assert_eq!(order_status.order_control_id, order_submit.order_control_id);
2938        assert_eq!(order_status.status, PlatformOrderControlStatus::Submitting);
2939    }
2940
2941    #[test]
2942    fn public_platform_response_rejects_unreviewed_fields() {
2943        let mut value: serde_json::Value =
2944            serde_json::from_str(PLATFORM_CAPABILITIES_FIXTURE).unwrap();
2945        value
2946            .as_object_mut()
2947            .unwrap()
2948            .insert("unexpected_field".to_owned(), serde_json::Value::Bool(true));
2949        assert!(serde_json::from_value::<PlatformDiscoveryResponse>(value).is_err());
2950
2951        let mut account_event: serde_json::Value =
2952            serde_json::from_str(PLATFORM_ACCOUNT_FIXTURE).unwrap();
2953        let event = account_event.as_object_mut().unwrap();
2954        event.insert("type".to_owned(), serde_json::json!("account_snapshot"));
2955        event.insert(
2956            "stream_id".to_owned(),
2957            serde_json::json!("account_stream_66666666666666666666666666666666"),
2958        );
2959        event.insert("sequence".to_owned(), serde_json::json!("1"));
2960        event.insert("unexpected_field".to_owned(), serde_json::json!(true));
2961        assert!(serde_json::from_value::<PlatformAccountEvent>(account_event).is_err());
2962    }
2963
2964    #[test]
2965    fn platform_graph_availability_is_projected_from_live_capabilities() {
2966        let mut graph = PlatformActionGraphResponse::foundation();
2967        let live = std::collections::BTreeSet::from([
2968            "platform.discover".to_owned(),
2969            "graphs.read".to_owned(),
2970            "orders.replace".to_owned(),
2971        ]);
2972
2973        graph.project_availability(&live);
2974
2975        for operation in &graph.operations {
2976            assert_eq!(
2977                operation.available,
2978                live.contains(&operation.capability_id),
2979                "operation {} did not follow capability {}",
2980                operation.id,
2981                operation.capability_id,
2982            );
2983        }
2984        assert!(graph
2985            .workflows
2986            .iter()
2987            .flat_map(|workflow| &workflow.nodes)
2988            .filter(|node| node.kind != PlatformActionKind::ExternalSignature)
2989            .all(|node| {
2990                node.available
2991                    == node
2992                        .capability_id
2993                        .as_ref()
2994                        .is_some_and(|capability_id| live.contains(capability_id))
2995            }));
2996        assert!(graph
2997            .workflows
2998            .iter()
2999            .flat_map(|workflow| &workflow.nodes)
3000            .filter(|node| node.kind == PlatformActionKind::ExternalSignature)
3001            .all(|node| node.available));
3002    }
3003
3004    #[test]
3005    fn atomic_order_batch_request_is_strict_and_typed() {
3006        let request: PlatformOrderChallengeRequest = serde_json::from_value(serde_json::json!({
3007            "action": "batch",
3008            "owner_wallet": "11111111111111111111111111111111",
3009            "session_public_key": "22222222222222222222222222222222",
3010            "operations": [
3011                {
3012                    "action": "cancel",
3013                    "order_id": "order_11111111111111111111111111111111"
3014                },
3015                {
3016                    "action": "replace",
3017                    "order_id": "order_22222222222222222222222222222222",
3018                    "account_sequence": "8",
3019                    "client_order_id": "replacement-8",
3020                    "side": "sell",
3021                    "order_type": "post_only",
3022                    "limit_price_atoms": "151000000",
3023                    "size_atoms": "2000000"
3024                }
3025            ]
3026        }))
3027        .unwrap();
3028        let PlatformOrderChallengeRequest::Batch { operations, .. } = request else {
3029            panic!("expected batch request");
3030        };
3031        assert_eq!(operations.len(), 2);
3032        assert!(matches!(
3033            &operations[1],
3034            PlatformOrderBatchOperation::Replace { account_sequence: Some(sequence), .. }
3035                if sequence == "8"
3036        ));
3037
3038        // The account sequence is optional: Strata resolves it from the Vault's
3039        // confirmed market account when omitted, and omitted stays omitted on
3040        // the wire so older servers reject rather than misread it.
3041        let place: PlatformOrderChallengeRequest = serde_json::from_value(serde_json::json!({
3042            "action": "place",
3043            "owner_wallet": "11111111111111111111111111111111",
3044            "session_public_key": "22222222222222222222222222222222",
3045            "client_order_id": "first-order",
3046            "side": "buy",
3047            "order_type": "post_only",
3048            "limit_price_atoms": "150000000",
3049            "size_atoms": "1000000"
3050        }))
3051        .unwrap();
3052        assert!(matches!(
3053            place,
3054            PlatformOrderChallengeRequest::Place {
3055                account_sequence: None,
3056                ..
3057            }
3058        ));
3059        assert!(!serde_json::to_string(&place)
3060            .unwrap()
3061            .contains("account_sequence"));
3062
3063        assert!(
3064            serde_json::from_value::<PlatformOrderChallengeRequest>(serde_json::json!({
3065                "action": "batch",
3066                "owner_wallet": "11111111111111111111111111111111",
3067                "session_public_key": "22222222222222222222222222222222",
3068                "operations": [{
3069                    "action": "cancel",
3070                    "order_id": "order_11111111111111111111111111111111",
3071                    "implementation": "hidden"
3072                }]
3073            }))
3074            .is_err()
3075        );
3076    }
3077
3078    #[test]
3079    fn persistent_order_commands_are_strict_and_explicit_about_self_trade_policy() {
3080        let frame: PlatformOrderCommandClientFrame = serde_json::from_value(serde_json::json!({
3081            "type": "command",
3082            "request_id": "agent-1",
3083            "sequence": "1",
3084            "command": {
3085                "type": "challenge",
3086                "self_trade_prevention": "cancel_maker",
3087                "request": {
3088                    "action": "cancel_all",
3089                    "owner_wallet": "11111111111111111111111111111111",
3090                    "session_public_key": "22222222222222222222222222222222"
3091                }
3092            }
3093        }))
3094        .unwrap();
3095        assert!(matches!(
3096            frame,
3097            PlatformOrderCommandClientFrame::Command {
3098                command: PlatformOrderCommand::Challenge {
3099                    self_trade_prevention: PlatformSelfTradePrevention::CancelMaker,
3100                    ..
3101                },
3102                ..
3103            }
3104        ));
3105        assert!(
3106            serde_json::from_value::<PlatformOrderCommandClientFrame>(serde_json::json!({
3107                "type": "command",
3108                "request_id": "agent-1",
3109                "sequence": "1",
3110                "command": {
3111                    "type": "challenge",
3112                    "request": {
3113                        "action": "cancel_all",
3114                        "owner_wallet": "11111111111111111111111111111111",
3115                        "session_public_key": "22222222222222222222222222222222"
3116                    }
3117                }
3118            }))
3119            .is_err()
3120        );
3121    }
3122
3123    #[test]
3124    fn prepare_requests_accept_a_signed_challenge_or_the_operation_itself() {
3125        let signed: PlatformOrderPrepareRequest = serde_json::from_value(serde_json::json!({
3126            "challenge_id": "oc_0123456789abcdef0123456789abcdef",
3127            "authorization_signature": "1111",
3128        }))
3129        .unwrap();
3130        assert!(matches!(signed, PlatformOrderPrepareRequest::Authorized(_)));
3131        let direct: PlatformOrderPrepareRequest = serde_json::from_value(serde_json::json!({
3132            "action": "cancel_all",
3133            "owner_wallet": "5Ji61Fbeb22Yntgv1hhHeSSLgdEdZchHeM1Tv1MjGhSL",
3134            "session_public_key": "9Uu7cLBgfMk233BAjMvTS8XJy6KbZK7oQ7NXuCTi3Fg2",
3135        }))
3136        .unwrap();
3137        assert!(matches!(
3138            direct,
3139            PlatformOrderPrepareRequest::Direct(PlatformOrderChallengeRequest::CancelAll { .. })
3140        ));
3141        // Neither shape tolerates a stray field.
3142        assert!(
3143            serde_json::from_value::<PlatformOrderPrepareRequest>(serde_json::json!({
3144                "challenge_id": "oc_0123456789abcdef0123456789abcdef",
3145                "authorization_signature": "1111",
3146                "extra": true,
3147            }))
3148            .is_err()
3149        );
3150        let twap: PlatformTwapPrepareRequest = serde_json::from_value(serde_json::json!({
3151            "action": "cancel",
3152            "owner_wallet": "5Ji61Fbeb22Yntgv1hhHeSSLgdEdZchHeM1Tv1MjGhSL",
3153            "session_public_key": "9Uu7cLBgfMk233BAjMvTS8XJy6KbZK7oQ7NXuCTi3Fg2",
3154            "twap_id": "twap_0123456789abcdef0123456789abcdef",
3155        }))
3156        .unwrap();
3157        assert!(matches!(twap, PlatformTwapPrepareRequest::Direct(_)));
3158        let execution: crate::ExecutionPrepareRequest = serde_json::from_value(serde_json::json!({
3159            "quote_id": "quote_0123456789abcdef0123456789abcdef",
3160            "owner_wallet": "5Ji61Fbeb22Yntgv1hhHeSSLgdEdZchHeM1Tv1MjGhSL",
3161            "session_public_key": "9Uu7cLBgfMk233BAjMvTS8XJy6KbZK7oQ7NXuCTi3Fg2",
3162        }))
3163        .unwrap();
3164        assert!(matches!(
3165            execution,
3166            crate::ExecutionPrepareRequest::Direct(_)
3167        ));
3168    }
3169
3170    #[test]
3171    fn maker_control_requests_are_tagged_exact_and_amount_safe() {
3172        let strand_upsert: PlatformMakerStrandPrepareRequest =
3173            serde_json::from_value(serde_json::json!({
3174                "action": "upsert",
3175                "maker_wallet": "5Ji61Fbeb22Yntgv1hhHeSSLgdEdZchHeM1Tv1MjGhSL",
3176                "enabled": true,
3177                "async_only": false,
3178                "sync_spread_ticks": 1,
3179                "mid_price_atoms": "123000000",
3180                "max_exposure_base_atoms": "1000000",
3181                "bid_offsets_ticks": vec![1; 16],
3182                "ask_offsets_ticks": vec![1; 16],
3183                "bid_sizes_base_atoms": vec!["1"; 16],
3184                "ask_sizes_base_atoms": vec!["1"; 16],
3185                "valid_until_slot": "0"
3186            }))
3187            .unwrap();
3188        let serialized = serde_json::to_value(&strand_upsert).unwrap();
3189        assert_eq!(serialized["max_exposure_base_atoms"], "1000000");
3190        assert!(serialized.get("max_exposure_base_lots").is_none());
3191
3192        // 0.2.1 clients remain accepted, but every response and current client
3193        // uses the corrected base-atom vocabulary.
3194        let legacy_strand: PlatformMakerStrandPrepareRequest =
3195            serde_json::from_value(serde_json::json!({
3196                "action": "upsert",
3197                "maker_wallet": "5Ji61Fbeb22Yntgv1hhHeSSLgdEdZchHeM1Tv1MjGhSL",
3198                "enabled": true,
3199                "async_only": false,
3200                "sync_spread_ticks": 1,
3201                "mid_price_atoms": "123000000",
3202                "max_exposure_base_lots": "1000000",
3203                "bid_offsets_ticks": vec![1; 16],
3204                "ask_offsets_ticks": vec![1; 16],
3205                "bid_sizes_base_lots": vec!["1"; 16],
3206                "ask_sizes_base_lots": vec!["1"; 16],
3207                "valid_until_slot": "0"
3208            }))
3209            .unwrap();
3210        assert_eq!(serde_json::to_value(legacy_strand).unwrap(), serialized);
3211
3212        let strand: PlatformMakerStrandPrepareRequest = serde_json::from_value(serde_json::json!({
3213            "action": "recenter",
3214            "maker_wallet": "5Ji61Fbeb22Yntgv1hhHeSSLgdEdZchHeM1Tv1MjGhSL",
3215            "new_mid_price_atoms": "123000000",
3216            "valid_until_slot": "0"
3217        }))
3218        .unwrap();
3219        assert!(matches!(
3220            strand,
3221            PlatformMakerStrandPrepareRequest::Recenter { .. }
3222        ));
3223
3224        let current: PlatformMakerCurrentPrepareRequest =
3225            serde_json::from_value(serde_json::json!({
3226                "action": "cancel",
3227                "maker_wallet": "5Ji61Fbeb22Yntgv1hhHeSSLgdEdZchHeM1Tv1MjGhSL"
3228            }))
3229            .unwrap();
3230        assert!(matches!(
3231            current,
3232            PlatformMakerCurrentPrepareRequest::Cancel { .. }
3233        ));
3234        assert!(
3235            serde_json::from_value::<PlatformMakerCurrentPrepareRequest>(serde_json::json!({
3236                "action": "cancel",
3237                "maker_wallet": "5Ji61Fbeb22Yntgv1hhHeSSLgdEdZchHeM1Tv1MjGhSL",
3238                "oracle_price": 123.45
3239            }))
3240            .is_err()
3241        );
3242    }
3243}