Skip to main content

strata_public_contract/
platform.rs

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