use serde::{Deserialize, Serialize};
use crate::{CapabilityRisk, McpExposure};
pub const PLATFORM_SCHEMA_VERSION: u16 = 2;
pub const PLATFORM_CONTRACT_VERSION: &str = "2.0";
#[cfg(any(test, feature = "fixtures"))]
#[doc(hidden)]
pub const PLATFORM_CAPABILITIES_FIXTURE: &str =
include_str!("../fixtures/v2/platform-capabilities.json");
#[cfg(any(test, feature = "fixtures"))]
#[doc(hidden)]
pub const PLATFORM_ASSETS_FIXTURE: &str = include_str!("../fixtures/v2/assets.json");
#[cfg(any(test, feature = "fixtures"))]
#[doc(hidden)]
pub const PLATFORM_MARKETS_FIXTURE: &str = include_str!("../fixtures/v2/markets.json");
#[cfg(any(test, feature = "fixtures"))]
#[doc(hidden)]
pub const PLATFORM_BOOK_FIXTURE: &str = include_str!("../fixtures/v2/book.json");
#[cfg(any(test, feature = "fixtures"))]
#[doc(hidden)]
pub const PLATFORM_BBO_FIXTURE: &str = include_str!("../fixtures/v2/bbo.json");
#[cfg(any(test, feature = "fixtures"))]
#[doc(hidden)]
pub const PLATFORM_FEES_FIXTURE: &str = include_str!("../fixtures/v2/fees.json");
#[cfg(any(test, feature = "fixtures"))]
#[doc(hidden)]
pub const PLATFORM_STATUS_FIXTURE: &str = include_str!("../fixtures/v2/status.json");
#[cfg(any(test, feature = "fixtures"))]
#[doc(hidden)]
pub const PLATFORM_TRADES_FIXTURE: &str = include_str!("../fixtures/v2/trades.json");
#[cfg(any(test, feature = "fixtures"))]
#[doc(hidden)]
pub const PLATFORM_ACCOUNT_FIXTURE: &str = include_str!("../fixtures/v2/account.json");
#[cfg(any(test, feature = "fixtures"))]
#[doc(hidden)]
pub const PLATFORM_ORDER_CHALLENGE_FIXTURE: &str =
include_str!("../fixtures/v2/order-challenge.json");
#[cfg(any(test, feature = "fixtures"))]
#[doc(hidden)]
pub const PLATFORM_ORDER_PREPARE_FIXTURE: &str = include_str!("../fixtures/v2/order-prepare.json");
#[cfg(any(test, feature = "fixtures"))]
#[doc(hidden)]
pub const PLATFORM_ORDER_SUBMIT_FIXTURE: &str = include_str!("../fixtures/v2/order-submit.json");
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PermissionSource {
ExternalAgentOwner,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SigningLocation {
External,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PlatformAuthority {
pub permission_source: PermissionSource,
pub signing_location: SigningLocation,
pub accepts_private_keys: bool,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PlatformTransport {
Http,
Websocket,
Mcp,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PlatformMarketState {
Active,
ReadOnly,
QuoteOnly,
CancelOnly,
Paused,
Warming,
Degraded,
Unavailable,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PlatformOrderState {
Created,
Accepted,
Open,
PartiallyFilled,
Filled,
CancelPending,
Cancelled,
Expired,
Rejected,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PlatformSettlementState {
NotApplicable,
Pending,
Confirmed,
Failed,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PlatformPublicErrorCode {
InvalidRequest,
UnsupportedCapability,
MarketUnavailable,
MarketWarming,
QuoteUnavailable,
QuoteExpired,
PriceBoundFailed,
InsufficientBalance,
PolicyRejected,
SessionExpired,
SequenceConflict,
DuplicateClientId,
OrderRejected,
OrderNotFound,
CancelTooLate,
RateLimited,
TemporarilyUnavailable,
SubmissionAmbiguous,
SettlementPending,
SettlementFailed,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ExactAmount {
pub asset_id: String,
pub atoms: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct SequenceEnvelope {
pub stream_id: String,
pub sequence: String,
pub previous_sequence: Option<String>,
pub server_time_ms: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub snapshot_id: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PageRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PageInfo {
pub next_cursor: Option<String>,
pub has_more: bool,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PublicOperationError {
pub code: PlatformPublicErrorCode,
pub message: String,
pub retryable: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub retry_after_ms: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub operation_id: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct LivePlatformCapability {
pub id: String,
pub risk: CapabilityRisk,
pub required_scope: String,
pub transports: Vec<PlatformTransport>,
pub mcp_exposure: McpExposure,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PlatformDiscoveryResponse {
pub schema_version: u16,
pub contract_version: String,
pub server_time_ms: u64,
pub authority: PlatformAuthority,
pub capabilities: Vec<LivePlatformCapability>,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PlatformNetwork {
Solana,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PlatformAsset {
pub asset_id: String,
pub symbol: String,
pub name: String,
pub decimals: u8,
#[serde(skip_serializing_if = "Option::is_none")]
pub logo_url: Option<String>,
pub network: PlatformNetwork,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PlatformAssetsResponse {
pub schema_version: u16,
pub contract_version: String,
pub server_time_ms: u64,
pub assets: Vec<PlatformAsset>,
pub page: PageInfo,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PlatformMarketAction {
Quote,
ExecuteImmediate,
PlaceOrder,
ScheduleTwap,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PlatformMarket {
pub market_id: String,
pub label: String,
pub base_asset_id: String,
pub quote_asset_id: String,
pub status: PlatformMarketState,
pub available_actions: Vec<PlatformMarketAction>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PlatformMarketsResponse {
pub schema_version: u16,
pub contract_version: String,
pub server_time_ms: u64,
pub markets: Vec<PlatformMarket>,
pub page: PageInfo,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PlatformBookLevel {
pub price_atoms: String,
pub size_atoms: String,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PlatformBookSide {
Bid,
Ask,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PlatformBookChange {
pub side: PlatformBookSide,
pub price_atoms: String,
pub size_atoms: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PlatformBookSnapshotResponse {
pub schema_version: u16,
pub contract_version: String,
pub market_id: String,
pub stream_id: String,
pub sequence: String,
pub server_time_ms: u64,
pub snapshot_id: String,
pub bids: Vec<PlatformBookLevel>,
pub asks: Vec<PlatformBookLevel>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PlatformBestBidAskResponse {
pub schema_version: u16,
pub contract_version: String,
pub market_id: String,
pub stream_id: String,
pub sequence: String,
pub server_time_ms: u64,
pub best_bid: Option<PlatformBookLevel>,
pub best_ask: Option<PlatformBookLevel>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PlatformFeeScheduleResponse {
pub schema_version: u16,
pub contract_version: String,
pub market_id: String,
pub server_time_ms: u64,
pub passive_maker_fee_bps: u16,
pub maximum_immediate_execution_fee_bps: u16,
pub book_prices_include_trading_fees: bool,
pub exact_fee_returned_by_quote: bool,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PlatformMarketStatusResponse {
pub schema_version: u16,
pub contract_version: String,
pub market_id: String,
pub server_time_ms: u64,
pub status: PlatformMarketState,
pub tick_size_atoms: String,
pub minimum_order_size_atoms: String,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PlatformTradeSide {
Buy,
Sell,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PlatformTrade {
pub trade_id: String,
pub side: PlatformTradeSide,
pub price_atoms: String,
pub size_atoms: String,
pub executed_at_ms: u64,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PlatformTradesResponse {
pub schema_version: u16,
pub contract_version: String,
pub market_id: String,
pub server_time_ms: u64,
pub trades: Vec<PlatformTrade>,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PlatformOrderType {
GoodUntilCancelled,
ImmediateOrCancel,
FillOrKill,
PostOnly,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PlatformOrderAction {
Place,
Cancel,
CancelAll,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(tag = "action", rename_all = "snake_case", deny_unknown_fields)]
pub enum PlatformOrderChallengeRequest {
Place {
owner_wallet: String,
session_public_key: String,
account_sequence: String,
client_order_id: String,
side: PlatformTradeSide,
order_type: PlatformOrderType,
limit_price_atoms: String,
size_atoms: String,
},
Cancel {
owner_wallet: String,
session_public_key: String,
order_id: String,
},
CancelAll {
owner_wallet: String,
session_public_key: String,
},
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PlatformOrderChallengeResponse {
pub schema_version: u16,
pub contract_version: String,
pub challenge_id: String,
pub market_id: String,
pub action: PlatformOrderAction,
pub order_ids: Vec<String>,
pub authorization_payload_base64: String,
pub server_time_ms: u64,
pub expires_at_ms: u64,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PlatformOrderPrepareRequest {
pub challenge_id: String,
pub authorization_signature: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PlatformOrderPrepareResponse {
pub schema_version: u16,
pub contract_version: String,
pub order_control_id: String,
pub market_id: String,
pub action: PlatformOrderAction,
pub order_ids: Vec<String>,
pub transaction_base64: String,
pub recent_blockhash: String,
pub last_valid_block_height: u64,
pub expires_at_ms: u64,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PlatformOrderSubmitRequest {
pub order_control_id: String,
pub signed_transaction_base64: String,
pub idempotency_key: String,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PlatformOrderSubmissionStatus {
Submitted,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PlatformOrderSubmitResponse {
pub schema_version: u16,
pub contract_version: String,
pub order_control_id: String,
pub market_id: String,
pub action: PlatformOrderAction,
pub order_ids: Vec<String>,
pub signature: String,
pub status: PlatformOrderSubmissionStatus,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PlatformAccountOrder {
pub order_id: String,
pub side: PlatformTradeSide,
pub order_type: PlatformOrderType,
pub state: PlatformOrderState,
pub limit_price_atoms: String,
pub original_size_atoms: String,
pub remaining_size_atoms: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PlatformAccountFill {
pub fill_id: String,
pub side: PlatformTradeSide,
pub price_atoms: String,
pub size_atoms: String,
pub fee_quote_atoms: String,
pub fee_is_final: bool,
pub settlement: PlatformSettlementState,
pub executed_at_ms: u64,
pub confirmed_at_ms: Option<u64>,
pub transaction_id: Option<String>,
pub realized_pnl_quote_atoms: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PlatformAccountSnapshotResponse {
pub schema_version: u16,
pub contract_version: String,
pub market_id: String,
pub wallet_address: String,
pub server_time_ms: u64,
pub orders: Vec<PlatformAccountOrder>,
pub fills: Vec<PlatformAccountFill>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
pub enum PlatformAccountEvent {
AuthChallenge {
schema_version: u16,
contract_version: String,
market_id: String,
wallet_address: String,
challenge: String,
server_time_ms: u64,
expires_at_ms: u64,
},
AccountSnapshot {
schema_version: u16,
contract_version: String,
market_id: String,
wallet_address: String,
stream_id: String,
sequence: String,
server_time_ms: u64,
orders: Vec<PlatformAccountOrder>,
fills: Vec<PlatformAccountFill>,
},
OrdersSnapshot {
schema_version: u16,
contract_version: String,
market_id: String,
wallet_address: String,
stream_id: String,
sequence: String,
previous_sequence: String,
server_time_ms: u64,
orders: Vec<PlatformAccountOrder>,
},
Fill {
schema_version: u16,
contract_version: String,
market_id: String,
wallet_address: String,
stream_id: String,
sequence: String,
previous_sequence: String,
server_time_ms: u64,
fill: PlatformAccountFill,
},
Heartbeat {
schema_version: u16,
contract_version: String,
market_id: String,
wallet_address: String,
stream_id: String,
sequence: String,
previous_sequence: String,
server_time_ms: u64,
},
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum PlatformMarketDataEvent {
BookSnapshot {
schema_version: u16,
contract_version: String,
market_id: String,
stream_id: String,
sequence: String,
server_time_ms: u64,
snapshot_id: String,
bids: Vec<PlatformBookLevel>,
asks: Vec<PlatformBookLevel>,
},
BookDelta {
schema_version: u16,
contract_version: String,
market_id: String,
stream_id: String,
sequence: String,
previous_sequence: String,
server_time_ms: u64,
changes: Vec<PlatformBookChange>,
},
BestBidAsk {
schema_version: u16,
contract_version: String,
market_id: String,
stream_id: String,
sequence: String,
server_time_ms: u64,
best_bid: Option<PlatformBookLevel>,
best_ask: Option<PlatformBookLevel>,
},
Trade {
schema_version: u16,
contract_version: String,
market_id: String,
server_time_ms: u64,
trade: PlatformTrade,
},
MarketStatus {
schema_version: u16,
contract_version: String,
market_id: String,
server_time_ms: u64,
status: PlatformMarketState,
},
Heartbeat {
schema_version: u16,
contract_version: String,
market_id: String,
server_time_ms: u64,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn public_platform_fixtures_decode_strictly() {
let discovery: PlatformDiscoveryResponse =
serde_json::from_str(PLATFORM_CAPABILITIES_FIXTURE).unwrap();
let assets: PlatformAssetsResponse = serde_json::from_str(PLATFORM_ASSETS_FIXTURE).unwrap();
let markets: PlatformMarketsResponse =
serde_json::from_str(PLATFORM_MARKETS_FIXTURE).unwrap();
let book: PlatformBookSnapshotResponse =
serde_json::from_str(PLATFORM_BOOK_FIXTURE).unwrap();
let bbo: PlatformBestBidAskResponse = serde_json::from_str(PLATFORM_BBO_FIXTURE).unwrap();
let fees: PlatformFeeScheduleResponse =
serde_json::from_str(PLATFORM_FEES_FIXTURE).unwrap();
let status: PlatformMarketStatusResponse =
serde_json::from_str(PLATFORM_STATUS_FIXTURE).unwrap();
let trades: PlatformTradesResponse = serde_json::from_str(PLATFORM_TRADES_FIXTURE).unwrap();
let account: PlatformAccountSnapshotResponse =
serde_json::from_str(PLATFORM_ACCOUNT_FIXTURE).unwrap();
let order_challenge: PlatformOrderChallengeResponse =
serde_json::from_str(PLATFORM_ORDER_CHALLENGE_FIXTURE).unwrap();
let order_prepare: PlatformOrderPrepareResponse =
serde_json::from_str(PLATFORM_ORDER_PREPARE_FIXTURE).unwrap();
let order_submit: PlatformOrderSubmitResponse =
serde_json::from_str(PLATFORM_ORDER_SUBMIT_FIXTURE).unwrap();
assert_eq!(discovery.schema_version, PLATFORM_SCHEMA_VERSION);
assert_eq!(discovery.capabilities.len(), 5);
assert!(!discovery.authority.accepts_private_keys);
assert_eq!(assets.assets.len(), 2);
assert_eq!(markets.markets.len(), 1);
assert_eq!(markets.markets[0].base_asset_id, assets.assets[0].asset_id);
assert_eq!(markets.markets[0].quote_asset_id, assets.assets[1].asset_id);
assert_eq!(book.sequence, "42");
assert_eq!(bbo.best_bid.unwrap().price_atoms, "149990000");
assert_eq!(fees.maximum_immediate_execution_fee_bps, 10);
assert_eq!(status.status, PlatformMarketState::Active);
assert_eq!(trades.trades.len(), 1);
assert_eq!(account.orders.len(), 1);
assert_eq!(account.fills.len(), 1);
assert_eq!(order_challenge.action, PlatformOrderAction::Place);
assert_eq!(order_prepare.order_ids, order_challenge.order_ids);
assert_eq!(order_submit.order_ids, order_challenge.order_ids);
}
#[test]
fn public_platform_response_rejects_unreviewed_fields() {
let mut value: serde_json::Value =
serde_json::from_str(PLATFORM_CAPABILITIES_FIXTURE).unwrap();
value
.as_object_mut()
.unwrap()
.insert("unexpected_field".to_owned(), serde_json::Value::Bool(true));
assert!(serde_json::from_value::<PlatformDiscoveryResponse>(value).is_err());
let mut account_event: serde_json::Value =
serde_json::from_str(PLATFORM_ACCOUNT_FIXTURE).unwrap();
let event = account_event.as_object_mut().unwrap();
event.insert("type".to_owned(), serde_json::json!("account_snapshot"));
event.insert(
"stream_id".to_owned(),
serde_json::json!("account_stream_66666666666666666666666666666666"),
);
event.insert("sequence".to_owned(), serde_json::json!("1"));
event.insert("unexpected_field".to_owned(), serde_json::json!(true));
assert!(serde_json::from_value::<PlatformAccountEvent>(account_event).is_err());
}
}