diff --git a/asyncapi.json b/asyncapi.json new file mode 100644 index 0000000..f03378d --- /dev/null +++ b/asyncapi.json @@ -0,0 +1,3177 @@ +{ + "asyncapi": "3.0.0", + "info": { + "title": "Lighter WebSocket API", + "version": "1.0.0", + "description": "Real-time market data, account state, and transaction submission for the zkLighter exchange. Hand-mirrored from https://apidocs.lighter.xyz/docs/websocket-reference. The schemas are intentionally permissive (`additionalProperties: true`, all channel-specific fields optional) so server-side additions do not invalidate generated clients.", + "contact": { + "name": "Lighter API docs", + "url": "https://apidocs.lighter.xyz/docs/websocket-reference" + } + }, + "defaultContentType": "application/json", + "servers": { + "mainnet": { + "host": "mainnet.zklighter.elliot.ai", + "pathname": "/stream", + "protocol": "wss", + "description": "Mainnet WebSocket gateway. Append `?readonly=true` to bypass IP region restrictions for read-only data." + }, + "testnet": { + "host": "testnet.zklighter.elliot.ai", + "pathname": "/stream", + "protocol": "wss", + "description": "Testnet WebSocket gateway." + } + }, + "channels": { + "order_book": { + "address": "order_book/{market_id}", + "title": "Order Book", + "description": "Order book snapshots and diffs for a given market. Snapshots ship on subscribe; subsequent messages are price-level diffs.", + "messages": { + "OrderBookSubscribed": { + "$ref": "#/components/messages/OrderBookSubscribed" + }, + "OrderBookUpdate": { + "$ref": "#/components/messages/OrderBookUpdate" + } + }, + "parameters": { + "market_id": { + "description": "Market index." + } + } + }, + "ticker": { + "address": "ticker/{market_id}", + "title": "Best Bid and Offer (BBO)", + "description": "Best bid/offer updates for a given market.", + "messages": { + "TickerSubscribed": { + "$ref": "#/components/messages/TickerSubscribed" + }, + "TickerUpdate": { + "$ref": "#/components/messages/TickerUpdate" + } + }, + "parameters": { + "market_id": { + "description": "Market index." + } + } + }, + "market_stats": { + "address": "market_stats/{market_id}", + "title": "Market Stats", + "description": "Per-market rolling stats (volume, price change, etc.). Pass `all` as the market id to receive every market on one subscription.", + "messages": { + "MarketStatsSubscribed": { + "$ref": "#/components/messages/MarketStatsSubscribed" + }, + "MarketStatsUpdate": { + "$ref": "#/components/messages/MarketStatsUpdate" + } + }, + "parameters": { + "market_id": { + "description": "Market index, or the literal string `all` to receive stats for every market." + } + } + }, + "spot_market_stats": { + "address": "spot_market_stats/{market_id}", + "title": "Spot Market Stats", + "description": "Per-spot-market rolling stats. Pass `all` to receive every spot market on one subscription.", + "messages": { + "SpotMarketStatsSubscribed": { + "$ref": "#/components/messages/SpotMarketStatsSubscribed" + }, + "SpotMarketStatsUpdate": { + "$ref": "#/components/messages/SpotMarketStatsUpdate" + } + }, + "parameters": { + "market_id": { + "description": "Spot market index, or the literal string `all`." + } + } + }, + "trade": { + "address": "trade/{market_id}", + "title": "Trade", + "description": "Public trade stream for a given market.", + "messages": { + "TradeSubscribed": { + "$ref": "#/components/messages/TradeSubscribed" + }, + "TradeUpdate": { + "$ref": "#/components/messages/TradeUpdate" + } + }, + "parameters": { + "market_id": { + "description": "Market index." + } + } + }, + "candle": { + "address": "candle/{market_id}/{resolution}", + "title": "Candlesticks", + "description": "Candlestick stream for a (market, resolution) pair.", + "messages": { + "CandleSubscribed": { + "$ref": "#/components/messages/CandleSubscribed" + }, + "CandleUpdate": { + "$ref": "#/components/messages/CandleUpdate" + } + }, + "parameters": { + "market_id": { + "description": "Market index." + }, + "resolution": { + "description": "Candle resolution, e.g. `1m`, `5m`, `1h`, `1d`." + } + } + }, + "height": { + "address": "height", + "title": "Height", + "description": "Latest L2 block height.", + "messages": { + "HeightSubscribed": { + "$ref": "#/components/messages/HeightSubscribed" + }, + "HeightUpdate": { + "$ref": "#/components/messages/HeightUpdate" + } + } + }, + "account_all": { + "address": "account_all/{account_id}", + "title": "Account All", + "description": "Combined account stream: orders, positions, trades, funding histories/rates, and pool shares.", + "messages": { + "AccountAllSubscribed": { + "$ref": "#/components/messages/AccountAllSubscribed" + }, + "AccountAllUpdate": { + "$ref": "#/components/messages/AccountAllUpdate" + } + }, + "parameters": { + "account_id": { + "description": "Account index." + } + } + }, + "account_market": { + "address": "account_market/{market_id}/{account_id}", + "title": "Account Market", + "description": "Per-market view of a specific account (orders, positions, trades restricted to one market).", + "messages": { + "AccountMarketSubscribed": { + "$ref": "#/components/messages/AccountMarketSubscribed" + }, + "AccountMarketUpdate": { + "$ref": "#/components/messages/AccountMarketUpdate" + } + }, + "parameters": { + "market_id": { + "description": "Market index." + }, + "account_id": { + "description": "Account index." + } + } + }, + "user_stats": { + "address": "user_stats/{account_id}", + "title": "Account Stats", + "description": "Aggregate stats for an account (collateral, portfolio value, etc.).", + "messages": { + "UserStatsSubscribed": { + "$ref": "#/components/messages/UserStatsSubscribed" + }, + "UserStatsUpdate": { + "$ref": "#/components/messages/UserStatsUpdate" + } + }, + "parameters": { + "account_id": { + "description": "Account index." + } + } + }, + "account_tx": { + "address": "account_tx/{account_id}", + "title": "Account Tx", + "description": "Transaction history for a specific account.", + "messages": { + "AccountTxSubscribed": { + "$ref": "#/components/messages/AccountTxSubscribed" + }, + "AccountTxUpdate": { + "$ref": "#/components/messages/AccountTxUpdate" + } + }, + "parameters": { + "account_id": { + "description": "Account index." + } + } + }, + "account_all_orders": { + "address": "account_all_orders/{account_id}", + "title": "Account All Orders", + "description": "All orders across markets for an account.", + "messages": { + "AccountAllOrdersSubscribed": { + "$ref": "#/components/messages/AccountAllOrdersSubscribed" + }, + "AccountAllOrdersUpdate": { + "$ref": "#/components/messages/AccountAllOrdersUpdate" + } + }, + "parameters": { + "account_id": { + "description": "Account index." + } + } + }, + "account_orders": { + "address": "account_orders/{market_id}/{account_id}", + "title": "Account Orders", + "description": "Orders for an account scoped to a single market.", + "messages": { + "AccountOrdersSubscribed": { + "$ref": "#/components/messages/AccountOrdersSubscribed" + }, + "AccountOrdersUpdate": { + "$ref": "#/components/messages/AccountOrdersUpdate" + } + }, + "parameters": { + "market_id": { + "description": "Market index." + }, + "account_id": { + "description": "Account index." + } + } + }, + "account_all_trades": { + "address": "account_all_trades/{account_id}", + "title": "Account All Trades", + "description": "All trades for an account across markets. Snapshot keys trades by market index; updates may emit a flat list.", + "messages": { + "AccountAllTradesSubscribed": { + "$ref": "#/components/messages/AccountAllTradesSubscribed" + }, + "AccountAllTradesUpdate": { + "$ref": "#/components/messages/AccountAllTradesUpdate" + } + }, + "parameters": { + "account_id": { + "description": "Account index." + } + } + }, + "account_all_positions": { + "address": "account_all_positions/{account_id}", + "title": "Account All Positions", + "description": "All positions for an account, keyed by market index.", + "messages": { + "AccountAllPositionsSubscribed": { + "$ref": "#/components/messages/AccountAllPositionsSubscribed" + }, + "AccountAllPositionsUpdate": { + "$ref": "#/components/messages/AccountAllPositionsUpdate" + } + }, + "parameters": { + "account_id": { + "description": "Account index." + } + } + }, + "account_all_assets": { + "address": "account_all_assets/{account_id}", + "title": "Account All Assets", + "description": "Per-asset balances for all spot markets for a specific account. `balance` is in coin terms, not USDC.", + "messages": { + "AccountAllAssetsSubscribed": { + "$ref": "#/components/messages/AccountAllAssetsSubscribed" + }, + "AccountAllAssetsUpdate": { + "$ref": "#/components/messages/AccountAllAssetsUpdate" + } + }, + "parameters": { + "account_id": { + "description": "Account index." + } + } + }, + "account_spot_avg_entry_prices": { + "address": "account_spot_avg_entry_prices/{account_id}", + "title": "Average Entry Prices", + "description": "Spot avg-entry-price stream. Each event accounts as a buy/sell at the index price; `last_trade_id` confirms the validity horizon.", + "messages": { + "AccountSpotAvgEntryPricesSubscribed": { + "$ref": "#/components/messages/AccountSpotAvgEntryPricesSubscribed" + }, + "AccountSpotAvgEntryPricesUpdate": { + "$ref": "#/components/messages/AccountSpotAvgEntryPricesUpdate" + } + }, + "parameters": { + "account_id": { + "description": "Account index." + } + } + }, + "notification": { + "address": "notification/{account_id}", + "title": "Notification", + "description": "Per-account notification stream.", + "messages": { + "NotificationSubscribed": { + "$ref": "#/components/messages/NotificationSubscribed" + }, + "NotificationUpdate": { + "$ref": "#/components/messages/NotificationUpdate" + } + }, + "parameters": { + "account_id": { + "description": "Account index." + } + } + }, + "pool_data": { + "address": "pool_data/{account_id}", + "title": "Pool Data", + "description": "Live data for a public pool account.", + "messages": { + "PoolDataSubscribed": { + "$ref": "#/components/messages/PoolDataSubscribed" + }, + "PoolDataUpdate": { + "$ref": "#/components/messages/PoolDataUpdate" + } + }, + "parameters": { + "account_id": { + "description": "Pool account index." + } + } + }, + "pool_info": { + "address": "pool_info/{account_id}", + "title": "Pool Info", + "description": "Public pool metadata.", + "messages": { + "PoolInfoSubscribed": { + "$ref": "#/components/messages/PoolInfoSubscribed" + }, + "PoolInfoUpdate": { + "$ref": "#/components/messages/PoolInfoUpdate" + } + }, + "parameters": { + "account_id": { + "description": "Pool account index." + } + } + }, + "_control": { + "address": "(connection)", + "title": "Connection control plane", + "description": "Frames not tied to a subscription address: client \u2192 server `subscribe`/`unsubscribe`/`ping`/`jsonapi/sendtx`/`jsonapi/sendtxbatch`, and server \u2192 client `connected`/`error`/`pong`/`jsonapi/*` responses.", + "messages": { + "SubscribeRequest": { + "$ref": "#/components/messages/SubscribeRequest" + }, + "UnsubscribeRequest": { + "$ref": "#/components/messages/UnsubscribeRequest" + }, + "Ping": { + "$ref": "#/components/messages/Ping" + }, + "SendTx": { + "$ref": "#/components/messages/SendTx" + }, + "SendTxBatch": { + "$ref": "#/components/messages/SendTxBatch" + }, + "Connected": { + "$ref": "#/components/messages/Connected" + }, + "ServerError": { + "$ref": "#/components/messages/ServerError" + }, + "Pong": { + "$ref": "#/components/messages/Pong" + }, + "TxResponse": { + "$ref": "#/components/messages/TxResponse" + } + } + } + }, + "operations": { + "subscribe_order_book": { + "action": "send", + "channel": { + "$ref": "#/channels/order_book" + }, + "summary": "Subscribe to `order_book/{market_id}`.", + "messages": [ + { + "$ref": "#/components/messages/SubscribeRequest" + } + ] + }, + "receive_order_book": { + "action": "receive", + "channel": { + "$ref": "#/channels/order_book" + }, + "summary": "Receive snapshot + updates for `order_book/{market_id}`.", + "messages": [ + { + "$ref": "#/channels/order_book/messages/OrderBookSubscribed" + }, + { + "$ref": "#/channels/order_book/messages/OrderBookUpdate" + } + ] + }, + "subscribe_ticker": { + "action": "send", + "channel": { + "$ref": "#/channels/ticker" + }, + "summary": "Subscribe to `ticker/{market_id}`.", + "messages": [ + { + "$ref": "#/components/messages/SubscribeRequest" + } + ] + }, + "receive_ticker": { + "action": "receive", + "channel": { + "$ref": "#/channels/ticker" + }, + "summary": "Receive snapshot + updates for `ticker/{market_id}`.", + "messages": [ + { + "$ref": "#/channels/ticker/messages/TickerSubscribed" + }, + { + "$ref": "#/channels/ticker/messages/TickerUpdate" + } + ] + }, + "subscribe_market_stats": { + "action": "send", + "channel": { + "$ref": "#/channels/market_stats" + }, + "summary": "Subscribe to `market_stats/{market_id}`.", + "messages": [ + { + "$ref": "#/components/messages/SubscribeRequest" + } + ] + }, + "receive_market_stats": { + "action": "receive", + "channel": { + "$ref": "#/channels/market_stats" + }, + "summary": "Receive snapshot + updates for `market_stats/{market_id}`.", + "messages": [ + { + "$ref": "#/channels/market_stats/messages/MarketStatsSubscribed" + }, + { + "$ref": "#/channels/market_stats/messages/MarketStatsUpdate" + } + ] + }, + "subscribe_spot_market_stats": { + "action": "send", + "channel": { + "$ref": "#/channels/spot_market_stats" + }, + "summary": "Subscribe to `spot_market_stats/{market_id}`.", + "messages": [ + { + "$ref": "#/components/messages/SubscribeRequest" + } + ] + }, + "receive_spot_market_stats": { + "action": "receive", + "channel": { + "$ref": "#/channels/spot_market_stats" + }, + "summary": "Receive snapshot + updates for `spot_market_stats/{market_id}`.", + "messages": [ + { + "$ref": "#/channels/spot_market_stats/messages/SpotMarketStatsSubscribed" + }, + { + "$ref": "#/channels/spot_market_stats/messages/SpotMarketStatsUpdate" + } + ] + }, + "subscribe_trade": { + "action": "send", + "channel": { + "$ref": "#/channels/trade" + }, + "summary": "Subscribe to `trade/{market_id}`.", + "messages": [ + { + "$ref": "#/components/messages/SubscribeRequest" + } + ] + }, + "receive_trade": { + "action": "receive", + "channel": { + "$ref": "#/channels/trade" + }, + "summary": "Receive snapshot + updates for `trade/{market_id}`.", + "messages": [ + { + "$ref": "#/channels/trade/messages/TradeSubscribed" + }, + { + "$ref": "#/channels/trade/messages/TradeUpdate" + } + ] + }, + "subscribe_candle": { + "action": "send", + "channel": { + "$ref": "#/channels/candle" + }, + "summary": "Subscribe to `candle/{market_id}/{resolution}`.", + "messages": [ + { + "$ref": "#/components/messages/SubscribeRequest" + } + ] + }, + "receive_candle": { + "action": "receive", + "channel": { + "$ref": "#/channels/candle" + }, + "summary": "Receive snapshot + updates for `candle/{market_id}/{resolution}`.", + "messages": [ + { + "$ref": "#/channels/candle/messages/CandleSubscribed" + }, + { + "$ref": "#/channels/candle/messages/CandleUpdate" + } + ] + }, + "subscribe_height": { + "action": "send", + "channel": { + "$ref": "#/channels/height" + }, + "summary": "Subscribe to `height`.", + "messages": [ + { + "$ref": "#/components/messages/SubscribeRequest" + } + ] + }, + "receive_height": { + "action": "receive", + "channel": { + "$ref": "#/channels/height" + }, + "summary": "Receive snapshot + updates for `height`.", + "messages": [ + { + "$ref": "#/channels/height/messages/HeightSubscribed" + }, + { + "$ref": "#/channels/height/messages/HeightUpdate" + } + ] + }, + "subscribe_account_all": { + "action": "send", + "channel": { + "$ref": "#/channels/account_all" + }, + "summary": "Subscribe to `account_all/{account_id}`.", + "messages": [ + { + "$ref": "#/components/messages/SubscribeRequest" + } + ] + }, + "receive_account_all": { + "action": "receive", + "channel": { + "$ref": "#/channels/account_all" + }, + "summary": "Receive snapshot + updates for `account_all/{account_id}`.", + "messages": [ + { + "$ref": "#/channels/account_all/messages/AccountAllSubscribed" + }, + { + "$ref": "#/channels/account_all/messages/AccountAllUpdate" + } + ] + }, + "subscribe_account_market": { + "action": "send", + "channel": { + "$ref": "#/channels/account_market" + }, + "summary": "Subscribe to `account_market/{market_id}/{account_id}`.", + "messages": [ + { + "$ref": "#/components/messages/SubscribeRequest" + } + ], + "security": [ + { + "$ref": "#/components/securitySchemes/bearerToken" + } + ] + }, + "receive_account_market": { + "action": "receive", + "channel": { + "$ref": "#/channels/account_market" + }, + "summary": "Receive snapshot + updates for `account_market/{market_id}/{account_id}`.", + "messages": [ + { + "$ref": "#/channels/account_market/messages/AccountMarketSubscribed" + }, + { + "$ref": "#/channels/account_market/messages/AccountMarketUpdate" + } + ] + }, + "subscribe_user_stats": { + "action": "send", + "channel": { + "$ref": "#/channels/user_stats" + }, + "summary": "Subscribe to `user_stats/{account_id}`.", + "messages": [ + { + "$ref": "#/components/messages/SubscribeRequest" + } + ] + }, + "receive_user_stats": { + "action": "receive", + "channel": { + "$ref": "#/channels/user_stats" + }, + "summary": "Receive snapshot + updates for `user_stats/{account_id}`.", + "messages": [ + { + "$ref": "#/channels/user_stats/messages/UserStatsSubscribed" + }, + { + "$ref": "#/channels/user_stats/messages/UserStatsUpdate" + } + ] + }, + "subscribe_account_tx": { + "action": "send", + "channel": { + "$ref": "#/channels/account_tx" + }, + "summary": "Subscribe to `account_tx/{account_id}`.", + "messages": [ + { + "$ref": "#/components/messages/SubscribeRequest" + } + ], + "security": [ + { + "$ref": "#/components/securitySchemes/bearerToken" + } + ] + }, + "receive_account_tx": { + "action": "receive", + "channel": { + "$ref": "#/channels/account_tx" + }, + "summary": "Receive snapshot + updates for `account_tx/{account_id}`.", + "messages": [ + { + "$ref": "#/channels/account_tx/messages/AccountTxSubscribed" + }, + { + "$ref": "#/channels/account_tx/messages/AccountTxUpdate" + } + ] + }, + "subscribe_account_all_orders": { + "action": "send", + "channel": { + "$ref": "#/channels/account_all_orders" + }, + "summary": "Subscribe to `account_all_orders/{account_id}`.", + "messages": [ + { + "$ref": "#/components/messages/SubscribeRequest" + } + ], + "security": [ + { + "$ref": "#/components/securitySchemes/bearerToken" + } + ] + }, + "receive_account_all_orders": { + "action": "receive", + "channel": { + "$ref": "#/channels/account_all_orders" + }, + "summary": "Receive snapshot + updates for `account_all_orders/{account_id}`.", + "messages": [ + { + "$ref": "#/channels/account_all_orders/messages/AccountAllOrdersSubscribed" + }, + { + "$ref": "#/channels/account_all_orders/messages/AccountAllOrdersUpdate" + } + ] + }, + "subscribe_account_orders": { + "action": "send", + "channel": { + "$ref": "#/channels/account_orders" + }, + "summary": "Subscribe to `account_orders/{market_id}/{account_id}`.", + "messages": [ + { + "$ref": "#/components/messages/SubscribeRequest" + } + ], + "security": [ + { + "$ref": "#/components/securitySchemes/bearerToken" + } + ] + }, + "receive_account_orders": { + "action": "receive", + "channel": { + "$ref": "#/channels/account_orders" + }, + "summary": "Receive snapshot + updates for `account_orders/{market_id}/{account_id}`.", + "messages": [ + { + "$ref": "#/channels/account_orders/messages/AccountOrdersSubscribed" + }, + { + "$ref": "#/channels/account_orders/messages/AccountOrdersUpdate" + } + ] + }, + "subscribe_account_all_trades": { + "action": "send", + "channel": { + "$ref": "#/channels/account_all_trades" + }, + "summary": "Subscribe to `account_all_trades/{account_id}`.", + "messages": [ + { + "$ref": "#/components/messages/SubscribeRequest" + } + ] + }, + "receive_account_all_trades": { + "action": "receive", + "channel": { + "$ref": "#/channels/account_all_trades" + }, + "summary": "Receive snapshot + updates for `account_all_trades/{account_id}`.", + "messages": [ + { + "$ref": "#/channels/account_all_trades/messages/AccountAllTradesSubscribed" + }, + { + "$ref": "#/channels/account_all_trades/messages/AccountAllTradesUpdate" + } + ] + }, + "subscribe_account_all_positions": { + "action": "send", + "channel": { + "$ref": "#/channels/account_all_positions" + }, + "summary": "Subscribe to `account_all_positions/{account_id}`.", + "messages": [ + { + "$ref": "#/components/messages/SubscribeRequest" + } + ] + }, + "receive_account_all_positions": { + "action": "receive", + "channel": { + "$ref": "#/channels/account_all_positions" + }, + "summary": "Receive snapshot + updates for `account_all_positions/{account_id}`.", + "messages": [ + { + "$ref": "#/channels/account_all_positions/messages/AccountAllPositionsSubscribed" + }, + { + "$ref": "#/channels/account_all_positions/messages/AccountAllPositionsUpdate" + } + ] + }, + "subscribe_account_all_assets": { + "action": "send", + "channel": { + "$ref": "#/channels/account_all_assets" + }, + "summary": "Subscribe to `account_all_assets/{account_id}`.", + "messages": [ + { + "$ref": "#/components/messages/SubscribeRequest" + } + ], + "security": [ + { + "$ref": "#/components/securitySchemes/bearerToken" + } + ] + }, + "receive_account_all_assets": { + "action": "receive", + "channel": { + "$ref": "#/channels/account_all_assets" + }, + "summary": "Receive snapshot + updates for `account_all_assets/{account_id}`.", + "messages": [ + { + "$ref": "#/channels/account_all_assets/messages/AccountAllAssetsSubscribed" + }, + { + "$ref": "#/channels/account_all_assets/messages/AccountAllAssetsUpdate" + } + ] + }, + "subscribe_account_spot_avg_entry_prices": { + "action": "send", + "channel": { + "$ref": "#/channels/account_spot_avg_entry_prices" + }, + "summary": "Subscribe to `account_spot_avg_entry_prices/{account_id}`.", + "messages": [ + { + "$ref": "#/components/messages/SubscribeRequest" + } + ], + "security": [ + { + "$ref": "#/components/securitySchemes/bearerToken" + } + ] + }, + "receive_account_spot_avg_entry_prices": { + "action": "receive", + "channel": { + "$ref": "#/channels/account_spot_avg_entry_prices" + }, + "summary": "Receive snapshot + updates for `account_spot_avg_entry_prices/{account_id}`.", + "messages": [ + { + "$ref": "#/channels/account_spot_avg_entry_prices/messages/AccountSpotAvgEntryPricesSubscribed" + }, + { + "$ref": "#/channels/account_spot_avg_entry_prices/messages/AccountSpotAvgEntryPricesUpdate" + } + ] + }, + "subscribe_notification": { + "action": "send", + "channel": { + "$ref": "#/channels/notification" + }, + "summary": "Subscribe to `notification/{account_id}`.", + "messages": [ + { + "$ref": "#/components/messages/SubscribeRequest" + } + ], + "security": [ + { + "$ref": "#/components/securitySchemes/bearerToken" + } + ] + }, + "receive_notification": { + "action": "receive", + "channel": { + "$ref": "#/channels/notification" + }, + "summary": "Receive snapshot + updates for `notification/{account_id}`.", + "messages": [ + { + "$ref": "#/channels/notification/messages/NotificationSubscribed" + }, + { + "$ref": "#/channels/notification/messages/NotificationUpdate" + } + ] + }, + "subscribe_pool_data": { + "action": "send", + "channel": { + "$ref": "#/channels/pool_data" + }, + "summary": "Subscribe to `pool_data/{account_id}`.", + "messages": [ + { + "$ref": "#/components/messages/SubscribeRequest" + } + ], + "security": [ + { + "$ref": "#/components/securitySchemes/bearerToken" + } + ] + }, + "receive_pool_data": { + "action": "receive", + "channel": { + "$ref": "#/channels/pool_data" + }, + "summary": "Receive snapshot + updates for `pool_data/{account_id}`.", + "messages": [ + { + "$ref": "#/channels/pool_data/messages/PoolDataSubscribed" + }, + { + "$ref": "#/channels/pool_data/messages/PoolDataUpdate" + } + ] + }, + "subscribe_pool_info": { + "action": "send", + "channel": { + "$ref": "#/channels/pool_info" + }, + "summary": "Subscribe to `pool_info/{account_id}`.", + "messages": [ + { + "$ref": "#/components/messages/SubscribeRequest" + } + ], + "security": [ + { + "$ref": "#/components/securitySchemes/bearerToken" + } + ] + }, + "receive_pool_info": { + "action": "receive", + "channel": { + "$ref": "#/channels/pool_info" + }, + "summary": "Receive snapshot + updates for `pool_info/{account_id}`.", + "messages": [ + { + "$ref": "#/channels/pool_info/messages/PoolInfoSubscribed" + }, + { + "$ref": "#/channels/pool_info/messages/PoolInfoUpdate" + } + ] + }, + "unsubscribe": { + "action": "send", + "channel": { + "$ref": "#/channels/_control" + }, + "summary": "Cancel an existing subscription.", + "messages": [ + { + "$ref": "#/components/messages/UnsubscribeRequest" + } + ] + }, + "ping": { + "action": "send", + "channel": { + "$ref": "#/channels/_control" + }, + "summary": "Application-level heartbeat (server replies with `Pong`).", + "messages": [ + { + "$ref": "#/components/messages/Ping" + } + ] + }, + "send_tx": { + "action": "send", + "channel": { + "$ref": "#/channels/_control" + }, + "summary": "Submit a single signed transaction.", + "messages": [ + { + "$ref": "#/components/messages/SendTx" + } + ] + }, + "send_tx_batch": { + "action": "send", + "channel": { + "$ref": "#/channels/_control" + }, + "summary": "Submit up to 15 signed transactions in a single frame.", + "messages": [ + { + "$ref": "#/components/messages/SendTxBatch" + } + ] + }, + "receive_connected": { + "action": "receive", + "channel": { + "$ref": "#/channels/_control" + }, + "summary": "Connection welcome frame.", + "messages": [ + { + "$ref": "#/channels/_control/messages/Connected" + } + ] + }, + "receive_error": { + "action": "receive", + "channel": { + "$ref": "#/channels/_control" + }, + "summary": "Server error frame.", + "messages": [ + { + "$ref": "#/channels/_control/messages/ServerError" + } + ] + }, + "receive_pong": { + "action": "receive", + "channel": { + "$ref": "#/channels/_control" + }, + "summary": "Reply to a client `Ping`.", + "messages": [ + { + "$ref": "#/channels/_control/messages/Pong" + } + ] + }, + "receive_tx_response": { + "action": "receive", + "channel": { + "$ref": "#/channels/_control" + }, + "summary": "Reply to `jsonapi/sendtx` or `jsonapi/sendtxbatch` (success or error).", + "messages": [ + { + "$ref": "#/channels/_control/messages/TxResponse" + } + ] + } + }, + "components": { + "messages": { + "OrderBookSubscribed": { + "name": "OrderBookSubscribed", + "title": "Order Book snapshot", + "summary": "Initial snapshot delivered after a successful `subscribe` to `order_book/{market_id}`.", + "x-message-type": "subscribed/order_book", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "order_book": { + "type": "object", + "additionalProperties": true, + "properties": { + "asks": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "bids": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "offset": { + "type": "integer" + } + } + } + } + } + }, + "OrderBookUpdate": { + "name": "OrderBookUpdate", + "title": "Order Book update", + "summary": "Live update for an existing subscription to `order_book/{market_id}`.", + "x-message-type": "update/order_book", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "order_book": { + "type": "object", + "additionalProperties": true, + "properties": { + "asks": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "bids": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "offset": { + "type": "integer" + } + } + } + } + } + }, + "TickerSubscribed": { + "name": "TickerSubscribed", + "title": "Best Bid and Offer (BBO) snapshot", + "summary": "Initial snapshot delivered after a successful `subscribe` to `ticker/{market_id}`.", + "x-message-type": "subscribed/ticker", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "ticker": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "TickerUpdate": { + "name": "TickerUpdate", + "title": "Best Bid and Offer (BBO) update", + "summary": "Live update for an existing subscription to `ticker/{market_id}`.", + "x-message-type": "update/ticker", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "ticker": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "MarketStatsSubscribed": { + "name": "MarketStatsSubscribed", + "title": "Market Stats snapshot", + "summary": "Initial snapshot delivered after a successful `subscribe` to `market_stats/{market_id}`.", + "x-message-type": "subscribed/market_stats", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "market_stats": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "MarketStatsUpdate": { + "name": "MarketStatsUpdate", + "title": "Market Stats update", + "summary": "Live update for an existing subscription to `market_stats/{market_id}`.", + "x-message-type": "update/market_stats", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "market_stats": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "SpotMarketStatsSubscribed": { + "name": "SpotMarketStatsSubscribed", + "title": "Spot Market Stats snapshot", + "summary": "Initial snapshot delivered after a successful `subscribe` to `spot_market_stats/{market_id}`.", + "x-message-type": "subscribed/spot_market_stats", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "spot_market_stats": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "SpotMarketStatsUpdate": { + "name": "SpotMarketStatsUpdate", + "title": "Spot Market Stats update", + "summary": "Live update for an existing subscription to `spot_market_stats/{market_id}`.", + "x-message-type": "update/spot_market_stats", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "spot_market_stats": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "TradeSubscribed": { + "name": "TradeSubscribed", + "title": "Trade snapshot", + "summary": "Initial snapshot delivered after a successful `subscribe` to `trade/{market_id}`.", + "x-message-type": "subscribed/trade", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "trades": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Trade" + } + } + } + } + }, + "TradeUpdate": { + "name": "TradeUpdate", + "title": "Trade update", + "summary": "Live update for an existing subscription to `trade/{market_id}`.", + "x-message-type": "update/trade", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "trades": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Trade" + } + } + } + } + }, + "CandleSubscribed": { + "name": "CandleSubscribed", + "title": "Candlesticks snapshot", + "summary": "Initial snapshot delivered after a successful `subscribe` to `candle/{market_id}/{resolution}`.", + "x-message-type": "subscribed/candle", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "candle": { + "type": "object", + "additionalProperties": true + }, + "resolution": { + "type": "string" + } + } + } + }, + "CandleUpdate": { + "name": "CandleUpdate", + "title": "Candlesticks update", + "summary": "Live update for an existing subscription to `candle/{market_id}/{resolution}`.", + "x-message-type": "update/candle", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "candle": { + "type": "object", + "additionalProperties": true + }, + "resolution": { + "type": "string" + } + } + } + }, + "HeightSubscribed": { + "name": "HeightSubscribed", + "title": "Height snapshot", + "summary": "Initial snapshot delivered after a successful `subscribe` to `height`.", + "x-message-type": "subscribed/height", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "height": { + "type": "integer" + } + } + } + }, + "HeightUpdate": { + "name": "HeightUpdate", + "title": "Height update", + "summary": "Live update for an existing subscription to `height`.", + "x-message-type": "update/height", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "height": { + "type": "integer" + } + } + } + }, + "AccountAllSubscribed": { + "name": "AccountAllSubscribed", + "title": "Account All snapshot", + "summary": "Initial snapshot delivered after a successful `subscribe` to `account_all/{account_id}`.", + "x-message-type": "subscribed/account_all", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "account_id": { + "type": "integer" + }, + "orders": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Order" + } + }, + "positions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Position" + } + }, + "trades": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Trade" + } + }, + "funding_histories": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PositionFunding" + } + }, + "funding_rates": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "shares": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PoolShares" + } + } + } + } + }, + "AccountAllUpdate": { + "name": "AccountAllUpdate", + "title": "Account All update", + "summary": "Live update for an existing subscription to `account_all/{account_id}`.", + "x-message-type": "update/account_all", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "account_id": { + "type": "integer" + }, + "orders": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Order" + } + }, + "positions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Position" + } + }, + "trades": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Trade" + } + }, + "funding_histories": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PositionFunding" + } + }, + "funding_rates": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "shares": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PoolShares" + } + } + } + } + }, + "AccountMarketSubscribed": { + "name": "AccountMarketSubscribed", + "title": "Account Market snapshot", + "summary": "Initial snapshot delivered after a successful `subscribe` to `account_market/{market_id}/{account_id}`.", + "x-message-type": "subscribed/account_market", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "account_id": { + "type": "integer" + }, + "market_id": { + "type": "integer" + }, + "orders": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Order" + } + }, + "positions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Position" + } + }, + "trades": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Trade" + } + } + } + } + }, + "AccountMarketUpdate": { + "name": "AccountMarketUpdate", + "title": "Account Market update", + "summary": "Live update for an existing subscription to `account_market/{market_id}/{account_id}`.", + "x-message-type": "update/account_market", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "account_id": { + "type": "integer" + }, + "market_id": { + "type": "integer" + }, + "orders": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Order" + } + }, + "positions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Position" + } + }, + "trades": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Trade" + } + } + } + } + }, + "UserStatsSubscribed": { + "name": "UserStatsSubscribed", + "title": "Account Stats snapshot", + "summary": "Initial snapshot delivered after a successful `subscribe` to `user_stats/{account_id}`.", + "x-message-type": "subscribed/user_stats", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "account_id": { + "type": "integer" + }, + "stats": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "UserStatsUpdate": { + "name": "UserStatsUpdate", + "title": "Account Stats update", + "summary": "Live update for an existing subscription to `user_stats/{account_id}`.", + "x-message-type": "update/user_stats", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "account_id": { + "type": "integer" + }, + "stats": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "AccountTxSubscribed": { + "name": "AccountTxSubscribed", + "title": "Account Tx snapshot", + "summary": "Initial snapshot delivered after a successful `subscribe` to `account_tx/{account_id}`.", + "x-message-type": "subscribed/account_tx", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "account_id": { + "type": "integer" + }, + "txs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Transaction" + } + } + } + } + }, + "AccountTxUpdate": { + "name": "AccountTxUpdate", + "title": "Account Tx update", + "summary": "Live update for an existing subscription to `account_tx/{account_id}`.", + "x-message-type": "update/account_tx", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "account_id": { + "type": "integer" + }, + "txs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Transaction" + } + } + } + } + }, + "AccountAllOrdersSubscribed": { + "name": "AccountAllOrdersSubscribed", + "title": "Account All Orders snapshot", + "summary": "Initial snapshot delivered after a successful `subscribe` to `account_all_orders/{account_id}`.", + "x-message-type": "subscribed/account_all_orders", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "account_id": { + "type": "integer" + }, + "orders": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Order" + } + } + } + } + }, + "AccountAllOrdersUpdate": { + "name": "AccountAllOrdersUpdate", + "title": "Account All Orders update", + "summary": "Live update for an existing subscription to `account_all_orders/{account_id}`.", + "x-message-type": "update/account_all_orders", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "account_id": { + "type": "integer" + }, + "orders": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Order" + } + } + } + } + }, + "AccountOrdersSubscribed": { + "name": "AccountOrdersSubscribed", + "title": "Account Orders snapshot", + "summary": "Initial snapshot delivered after a successful `subscribe` to `account_orders/{market_id}/{account_id}`.", + "x-message-type": "subscribed/account_orders", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "account_id": { + "type": "integer" + }, + "market_id": { + "type": "integer" + }, + "orders": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Order" + } + } + } + } + }, + "AccountOrdersUpdate": { + "name": "AccountOrdersUpdate", + "title": "Account Orders update", + "summary": "Live update for an existing subscription to `account_orders/{market_id}/{account_id}`.", + "x-message-type": "update/account_orders", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "account_id": { + "type": "integer" + }, + "market_id": { + "type": "integer" + }, + "orders": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Order" + } + } + } + } + }, + "AccountAllTradesSubscribed": { + "name": "AccountAllTradesSubscribed", + "title": "Account All Trades snapshot", + "summary": "Initial snapshot delivered after a successful `subscribe` to `account_all_trades/{account_id}`.", + "x-message-type": "subscribed/account_all_trades", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "account_id": { + "type": "integer" + }, + "trades": { + "oneOf": [ + { + "type": "array", + "items": { + "$ref": "#/components/schemas/Trade" + } + }, + { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Trade" + } + } + } + ] + }, + "total_volume": { + "type": "number" + }, + "monthly_volume": { + "type": "number" + } + } + } + }, + "AccountAllTradesUpdate": { + "name": "AccountAllTradesUpdate", + "title": "Account All Trades update", + "summary": "Live update for an existing subscription to `account_all_trades/{account_id}`.", + "x-message-type": "update/account_all_trades", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "account_id": { + "type": "integer" + }, + "trades": { + "oneOf": [ + { + "type": "array", + "items": { + "$ref": "#/components/schemas/Trade" + } + }, + { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Trade" + } + } + } + ] + }, + "total_volume": { + "type": "number" + }, + "monthly_volume": { + "type": "number" + } + } + } + }, + "AccountAllPositionsSubscribed": { + "name": "AccountAllPositionsSubscribed", + "title": "Account All Positions snapshot", + "summary": "Initial snapshot delivered after a successful `subscribe` to `account_all_positions/{account_id}`.", + "x-message-type": "subscribed/account_all_positions", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "account_id": { + "type": "integer" + }, + "positions": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Position" + } + } + } + } + }, + "AccountAllPositionsUpdate": { + "name": "AccountAllPositionsUpdate", + "title": "Account All Positions update", + "summary": "Live update for an existing subscription to `account_all_positions/{account_id}`.", + "x-message-type": "update/account_all_positions", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "account_id": { + "type": "integer" + }, + "positions": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Position" + } + } + } + } + }, + "AccountAllAssetsSubscribed": { + "name": "AccountAllAssetsSubscribed", + "title": "Account All Assets snapshot", + "summary": "Initial snapshot delivered after a successful `subscribe` to `account_all_assets/{account_id}`.", + "x-message-type": "subscribed/account_all_assets", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "account_id": { + "type": "integer" + }, + "assets": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Asset" + } + } + } + } + }, + "AccountAllAssetsUpdate": { + "name": "AccountAllAssetsUpdate", + "title": "Account All Assets update", + "summary": "Live update for an existing subscription to `account_all_assets/{account_id}`.", + "x-message-type": "update/account_all_assets", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "account_id": { + "type": "integer" + }, + "assets": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Asset" + } + } + } + } + }, + "AccountSpotAvgEntryPricesSubscribed": { + "name": "AccountSpotAvgEntryPricesSubscribed", + "title": "Average Entry Prices snapshot", + "summary": "Initial snapshot delivered after a successful `subscribe` to `account_spot_avg_entry_prices/{account_id}`.", + "x-message-type": "subscribed/account_spot_avg_entry_prices", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "account_id": { + "type": "integer" + }, + "avg_entry_prices": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": true, + "properties": { + "asset_id": { + "type": "integer" + }, + "avg_entry_price": { + "type": "string" + }, + "asset_size": { + "type": "string" + }, + "last_trade_id": { + "type": "integer" + } + } + } + } + } + } + }, + "AccountSpotAvgEntryPricesUpdate": { + "name": "AccountSpotAvgEntryPricesUpdate", + "title": "Average Entry Prices update", + "summary": "Live update for an existing subscription to `account_spot_avg_entry_prices/{account_id}`.", + "x-message-type": "update/account_spot_avg_entry_prices", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "account_id": { + "type": "integer" + }, + "avg_entry_prices": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": true, + "properties": { + "asset_id": { + "type": "integer" + }, + "avg_entry_price": { + "type": "string" + }, + "asset_size": { + "type": "string" + }, + "last_trade_id": { + "type": "integer" + } + } + } + } + } + } + }, + "NotificationSubscribed": { + "name": "NotificationSubscribed", + "title": "Notification snapshot", + "summary": "Initial snapshot delivered after a successful `subscribe` to `notification/{account_id}`.", + "x-message-type": "subscribed/notification", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "account_id": { + "type": "integer" + }, + "notifications": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "NotificationUpdate": { + "name": "NotificationUpdate", + "title": "Notification update", + "summary": "Live update for an existing subscription to `notification/{account_id}`.", + "x-message-type": "update/notification", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "account_id": { + "type": "integer" + }, + "notifications": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "PoolDataSubscribed": { + "name": "PoolDataSubscribed", + "title": "Pool Data snapshot", + "summary": "Initial snapshot delivered after a successful `subscribe` to `pool_data/{account_id}`.", + "x-message-type": "subscribed/pool_data", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "account_id": { + "type": "integer" + } + } + } + }, + "PoolDataUpdate": { + "name": "PoolDataUpdate", + "title": "Pool Data update", + "summary": "Live update for an existing subscription to `pool_data/{account_id}`.", + "x-message-type": "update/pool_data", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "account_id": { + "type": "integer" + } + } + } + }, + "PoolInfoSubscribed": { + "name": "PoolInfoSubscribed", + "title": "Pool Info snapshot", + "summary": "Initial snapshot delivered after a successful `subscribe` to `pool_info/{account_id}`.", + "x-message-type": "subscribed/pool_info", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "account_id": { + "type": "integer" + } + } + } + }, + "PoolInfoUpdate": { + "name": "PoolInfoUpdate", + "title": "Pool Info update", + "summary": "Live update for an existing subscription to `pool_info/{account_id}`.", + "x-message-type": "update/pool_info", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "account_id": { + "type": "integer" + } + } + } + }, + "Connected": { + "name": "Connected", + "title": "Connection welcome", + "summary": "Sent once when the WebSocket connection is established.", + "x-message-type": "connected", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "const": "connected" + } + } + } + }, + "ServerError": { + "name": "ServerError", + "title": "Server error", + "summary": "Server-emitted error frame.", + "x-message-type": "error", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "const": "error" + }, + "message": { + "type": "string" + }, + "code": { + "type": "integer" + } + } + } + }, + "Pong": { + "name": "Pong", + "title": "Application-level pong", + "summary": "Reply to a client-sent `ping` frame.", + "x-message-type": "pong", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "const": "pong" + } + } + } + }, + "TxResponse": { + "name": "TxResponse", + "title": "Transaction submission response", + "summary": "Reply to `jsonapi/sendtx` or `jsonapi/sendtxbatch`.", + "x-message-type": "jsonapi/sendtx", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "code": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "tx_hash": { + "type": "string" + }, + "tx_hashes": { + "type": "array", + "items": { + "type": "string" + } + }, + "error": {} + } + } + }, + "SubscribeRequest": { + "name": "SubscribeRequest", + "title": "Subscribe", + "summary": "Open a subscription on a channel.", + "x-message-type": "subscribe", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type", + "channel" + ], + "properties": { + "type": { + "const": "subscribe" + }, + "channel": { + "type": "string", + "description": "Channel address. Use `/` as the path separator (e.g. `order_book/0`)." + }, + "auth": { + "type": "string", + "description": "Bearer token. Required for the channels listed under `securitySchemes.bearerToken`." + } + } + } + }, + "UnsubscribeRequest": { + "name": "UnsubscribeRequest", + "title": "Unsubscribe", + "summary": "Cancel an existing subscription.", + "x-message-type": "unsubscribe", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type", + "channel" + ], + "properties": { + "type": { + "const": "unsubscribe" + }, + "channel": { + "type": "string" + } + } + } + }, + "Ping": { + "name": "Ping", + "title": "Application-level ping", + "summary": "Heartbeat frame. The server replies with a `Pong`. Either WebSocket-level ping frames or this application-level frame satisfy the 2-minute idle requirement.", + "x-message-type": "ping", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "const": "ping" + } + } + } + }, + "SendTx": { + "name": "SendTx", + "title": "Send transaction", + "summary": "Submit a single signed transaction over the socket.", + "x-message-type": "jsonapi/sendtx", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type", + "data" + ], + "properties": { + "type": { + "const": "jsonapi/sendtx" + }, + "data": { + "type": "object", + "additionalProperties": true, + "required": [ + "tx_type", + "tx_info" + ], + "properties": { + "tx_type": { + "type": "integer" + }, + "tx_info": { + "description": "Signed payload produced by SignerClient. Usually a JSON-encoded string." + } + } + } + } + } + }, + "SendTxBatch": { + "name": "SendTxBatch", + "title": "Send transaction batch", + "summary": "Submit up to 15 signed transactions in one message. `tx_infos` is a JSON-encoded list of JSON-encoded `tx_info` strings (double-encoded).", + "x-message-type": "jsonapi/sendtxbatch", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": true, + "required": [ + "type", + "data" + ], + "properties": { + "type": { + "const": "jsonapi/sendtxbatch" + }, + "data": { + "type": "object", + "additionalProperties": true, + "required": [ + "tx_types", + "tx_infos" + ], + "properties": { + "tx_types": { + "type": "string", + "description": "JSON-encoded list of integer tx types, e.g. `\"[14,14]\"`." + }, + "tx_infos": { + "type": "string", + "description": "JSON-encoded list of JSON-encoded tx_info strings." + } + } + } + } + } + } + }, + "schemas": { + "Transaction": { + "type": "object", + "additionalProperties": true, + "description": "Transaction as emitted on the `account_tx` channel.", + "properties": { + "hash": { + "type": "string" + }, + "type": { + "type": "integer" + }, + "info": { + "type": "string", + "description": "JSON object encoded as string; shape depends on tx type." + }, + "event_info": { + "type": "string", + "description": "JSON object encoded as string; shape depends on tx type." + }, + "status": { + "type": "integer" + }, + "transaction_index": { + "type": "integer" + }, + "l1_address": { + "type": "string" + }, + "account_index": { + "type": "integer" + }, + "nonce": { + "type": "integer" + }, + "expire_at": { + "type": "integer" + }, + "block_height": { + "type": "integer" + }, + "queued_at": { + "type": "integer" + }, + "executed_at": { + "type": "integer" + }, + "sequence_index": { + "type": "integer" + }, + "parent_hash": { + "type": "string" + }, + "api_key_index": { + "type": "integer" + }, + "transaction_time": { + "type": "integer" + } + } + }, + "Order": { + "type": "object", + "additionalProperties": true, + "properties": { + "order_index": { + "type": "integer" + }, + "client_order_index": { + "type": "integer" + }, + "order_id": { + "type": "string" + }, + "client_order_id": { + "type": "string" + }, + "market_index": { + "type": "integer" + }, + "owner_account_index": { + "type": "integer" + }, + "initial_base_amount": { + "type": "string" + }, + "price": { + "type": "string" + }, + "nonce": { + "type": "integer" + }, + "remaining_base_amount": { + "type": "string" + }, + "is_ask": { + "type": "boolean" + }, + "base_size": { + "type": "integer" + }, + "base_price": { + "type": "integer" + }, + "filled_base_amount": { + "type": "string" + }, + "filled_quote_amount": { + "type": "string" + }, + "side": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "limit", + "market", + "stop-loss", + "stop-loss-limit", + "take-profit", + "take-profit-limit", + "twap", + "twap-sub", + "liquidation" + ] + }, + "time_in_force": { + "type": "string", + "enum": [ + "good-till-time", + "immediate-or-cancel", + "post-only", + "Unknown" + ] + }, + "reduce_only": { + "type": "boolean" + }, + "trigger_price": { + "type": "string" + }, + "order_expiry": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "trigger_status": { + "type": "string" + }, + "trigger_time": { + "type": "integer" + }, + "parent_order_index": { + "type": "integer" + }, + "parent_order_id": { + "type": "string" + }, + "to_trigger_order_id_0": { + "type": "string" + }, + "to_trigger_order_id_1": { + "type": "string" + }, + "to_cancel_order_id_0": { + "type": "string" + }, + "integrator_fee_collector_index": { + "type": "string" + }, + "integrator_taker_fee": { + "type": "string" + }, + "integrator_maker_fee": { + "type": "string" + }, + "block_height": { + "type": "integer" + } + } + }, + "Trade": { + "type": "object", + "additionalProperties": true, + "properties": { + "trade_id": { + "type": "integer" + }, + "market_id": { + "type": "integer" + }, + "size": { + "type": "string" + }, + "price": { + "type": "string" + }, + "usd_amount": { + "type": "string" + }, + "ask_id": { + "type": "integer" + }, + "ask_account_id": { + "type": "integer" + }, + "bid_id": { + "type": "integer" + }, + "bid_account_id": { + "type": "integer" + }, + "is_maker_ask": { + "type": "boolean" + }, + "block_height": { + "type": "integer" + }, + "timestamp": { + "type": "integer" + }, + "type": { + "type": "string" + } + } + }, + "Position": { + "type": "object", + "additionalProperties": true, + "properties": { + "market_id": { + "type": "integer" + }, + "sign": { + "type": "integer" + }, + "position": { + "type": "string" + }, + "avg_entry_price": { + "type": "string" + }, + "position_value": { + "type": "string" + }, + "unrealized_pnl": { + "type": "string" + }, + "realized_pnl": { + "type": "string" + }, + "margin_mode": { + "type": "integer" + }, + "allocated_margin": { + "type": "string" + }, + "liquidation_price": { + "type": "string" + } + } + }, + "PoolShares": { + "type": "object", + "additionalProperties": true, + "properties": { + "pool_account_index": { + "type": "integer" + }, + "owner_account_index": { + "type": "integer" + }, + "shares_amount": { + "type": "string" + }, + "entry_usdc_amount": { + "type": "string" + } + } + }, + "Asset": { + "type": "object", + "additionalProperties": true, + "properties": { + "symbol": { + "type": "string" + }, + "asset_id": { + "type": "integer" + }, + "balance": { + "type": "string" + }, + "locked_balance": { + "type": "string" + } + } + }, + "PositionFunding": { + "type": "object", + "additionalProperties": true, + "properties": { + "timestamp": { + "type": "integer" + }, + "market_id": { + "type": "integer" + }, + "funding_id": { + "type": "integer" + }, + "change": { + "type": "string" + }, + "rate": { + "type": "string" + }, + "position_size": { + "type": "string" + }, + "position_side": { + "type": "string", + "enum": [ + "long", + "short" + ] + }, + "discount": { + "type": "string" + } + } + } + }, + "securitySchemes": { + "bearerToken": { + "type": "http", + "scheme": "bearer", + "description": "Per-channel auth token passed in the `auth` field of the `subscribe` message. Required for channels whose subscribe operation lists this scheme under `security`. See the `apikeys` REST endpoint for token generation." + } + } + } +} diff --git a/examples/ws.py b/examples/ws.py index 8fc6445..cfa87be 100644 --- a/examples/ws.py +++ b/examples/ws.py @@ -5,19 +5,23 @@ logging.basicConfig(level=logging.INFO) -def on_order_book_update(market_id, order_book): - logging.info(f"Order book {market_id}:\n{json.dumps(order_book, indent=2)}") +def on_order_book(message): + logging.info( + f"Order book {message['channel']}:\n" + f"{json.dumps(message.get('order_book'), indent=2)}" + ) -def on_account_update(account_id, account): - logging.info(f"Account {account_id}:\n{json.dumps(account, indent=2)}") +def on_account(message): + logging.info( + f"Account {message['channel']}:\n{json.dumps(message, indent=2)}" + ) -client = lighter.WsClient( - order_book_ids=[0, 1], - account_ids=[1, 2], - on_order_book_update=on_order_book_update, - on_account_update=on_account_update, -) +client = lighter.WsClient() +for market_id in [0, 1]: + client.subscribe(f"order_book/{market_id}", on_update=on_order_book) +for account_id in [1, 2]: + client.subscribe(f"account_all/{account_id}", on_update=on_account) client.run() diff --git a/examples/ws_async.py b/examples/ws_async.py deleted file mode 100644 index 3262192..0000000 --- a/examples/ws_async.py +++ /dev/null @@ -1,24 +0,0 @@ -import json -import logging -import asyncio -import lighter - -logging.basicConfig(level=logging.INFO) - - -def on_order_book_update(market_id, order_book): - logging.info(f"Order book {market_id}:\n{json.dumps(order_book, indent=2)}") - - -def on_account_update(account_id, account): - logging.info(f"Account {account_id}:\n{json.dumps(account, indent=2)}") - - -client = lighter.WsClient( - order_book_ids=[0, 1], - account_ids=[1, 2], - on_order_book_update=on_order_book_update, - on_account_update=on_account_update, -) - -asyncio.run(client.run_async()) diff --git a/lighter/ws_client.py b/lighter/ws_client.py index 1369417..11db176 100644 --- a/lighter/ws_client.py +++ b/lighter/ws_client.py @@ -1,168 +1,474 @@ +"""Async WebSocket client for the Lighter API. + +Implements the channels and message types documented at +https://apidocs.lighter.xyz/docs/websocket-reference. + +The client exposes a single uniform :meth:`WsClient.subscribe` method for +all channels: callers pass the full channel string (e.g. ``"order_book/0"``, +``"trade/0"``, ``"candle/0/1m"``, ``"market_stats/all"``, +``"account_all/123"``) and an ``on_update`` callback. Authenticated +channels receive a default auth token from the client (overridable per +subscription). + +Transaction submission is supported via :meth:`WsClient.send_tx` and +:meth:`WsClient.send_tx_batch` which wrap the ``jsonapi/sendtx`` and +``jsonapi/sendtxbatch`` envelopes. +""" + +from __future__ import annotations + +import asyncio import json -from websockets.sync.client import connect -from websockets.client import connect as connect_async +import logging +from contextlib import suppress +from dataclasses import dataclass +from typing import ( + Any, + Awaitable, + Callable, + Coroutine, + Dict, + Iterable, + List, + Mapping, + Optional, + Tuple, + Union, +) + +try: + from websockets.asyncio.client import connect as _ws_connect +except ImportError: # pragma: no cover - websockets 12.x fallback + from websockets.client import connect as _ws_connect + from lighter.configuration import Configuration +from lighter.ws_messages import parse_ws_message + +logger = logging.getLogger(__name__) + +# Public callback type alias - callbacks may be either sync or async. +Callback = Callable[..., Union[None, Awaitable[None]]] + +# Channels that require an ``auth`` token alongside the subscribe message. +_AUTH_REQUIRED_PREFIXES: Tuple[str, ...] = ( + "account_market/", + "account_tx/", + "account_all_orders/", + "account_orders/", + "account_all_assets/", + "account_spot_avg_entry_prices/", + "notification/", + "pool_data/", + "pool_info/", +) + + +@dataclass +class _Subscription: + """A registered channel subscription.""" + + channel: str + on_update: Optional[Callback] = None + auth: Optional[str] = None + parse: bool = False + class WsClient: + """High-level async WebSocket client for the Lighter API. + + Parameters + ---------- + host + Hostname of the Lighter API (without scheme). Defaults to the host + of :class:`lighter.Configuration`. + path + WebSocket path (default ``"/stream"``). + readonly + If ``True`` connect with the ``?readonly=true`` query parameter. + auth + Default auth token used for channels under the documented + auth-required prefixes when no per-subscription token is provided. + ping_interval, ping_timeout + Forwarded to :func:`websockets.connect` for WebSocket-level + keepalive. The Lighter server closes connections that send no + frames for two minutes, so ``ping_interval`` should remain well + below that. + auto_reconnect + If ``True``, the run loop reconnects when the connection drops. + Registered subscriptions are re-sent on each reconnect. + reconnect_delay + Seconds to wait between reconnect attempts. + on_message + Optional callback invoked for any server message that does not + match a registered subscription (e.g. the ``connected`` welcome + message or unknown channels). + on_tx_response + Optional callback invoked for every ``jsonapi/*`` server message + (transaction send responses and errors). + """ + + DEFAULT_PATH = "/stream" + def __init__( self, - host=None, - path="/stream", - order_book_ids=[], - account_ids=[], - on_order_book_update=print, - on_account_update=print, - ): + host: Optional[str] = None, + *, + path: str = DEFAULT_PATH, + readonly: bool = False, + auth: Optional[str] = None, + ping_interval: Optional[float] = 30.0, + ping_timeout: Optional[float] = 60.0, + auto_reconnect: bool = False, + reconnect_delay: float = 1.0, + on_message: Optional[Callback] = None, + on_tx_response: Optional[Callback] = None, + ) -> None: if host is None: - host = Configuration.get_default().host.replace("https://", "") + default_host = Configuration.get_default().host + host = default_host.replace("https://", "").replace("http://", "") - self.base_url = f"wss://{host}{path}" + self.host = host + self.path = path + self.readonly = readonly + self.auth = auth + self.ping_interval = ping_interval + self.ping_timeout = ping_timeout + self.auto_reconnect = auto_reconnect + self.reconnect_delay = reconnect_delay + self.on_message = on_message + self.on_tx_response = on_tx_response - self.subscriptions = { - "order_books": order_book_ids, - "accounts": account_ids, - } + query = "?readonly=true" if readonly else "" + self.base_url = f"wss://{host}{path}{query}" - if len(order_book_ids) == 0 and len(account_ids) == 0: - raise Exception("No subscriptions provided.") + # Registered subscriptions keyed by canonical channel name (using + # ``/`` separators throughout for parity with the docs). + self._subscriptions: Dict[str, _Subscription] = {} - self.order_book_states = {} - self.account_states = {} + # Reconstructed order book snapshots, keyed by market_id. The + # client merges snapshot+diff messages for any ``order_book/*`` + # subscription so that callers can read the current book without + # having to maintain state themselves. + self.order_book_states: Dict[int, Dict[str, List[Dict[str, Any]]]] = {} - self.on_order_book_update = on_order_book_update - self.on_account_update = on_account_update + self.ws: Optional[Any] = None + self._send_lock: Optional[asyncio.Lock] = None + self._stopped = False - self.ws = None + # ------------------------------------------------------------------ + # Subscription management + # ------------------------------------------------------------------ - def on_message(self, ws, message): - if isinstance(message, str): - message = json.loads(message) + def subscribe( + self, + channel: str, + on_update: Optional[Callback] = None, + *, + auth: Optional[str] = None, + parse: bool = False, + ) -> None: + """Register a subscription for ``channel``. - message_type = message.get("type") + The subscription is sent to the server on connect (and re-sent on + each reconnect when :attr:`auto_reconnect` is enabled). If the + client is already connected when :meth:`subscribe` is called the + subscribe frame is dispatched immediately via the running event + loop. - if message_type == "connected": - self.handle_connected(ws) - elif message_type == "subscribed/order_book": - self.handle_subscribed_order_book(message) - elif message_type == "update/order_book": - self.handle_update_order_book(message) - elif message_type == "subscribed/account_all": - self.handle_subscribed_account(message) - elif message_type == "update/account_all": - self.handle_update_account(message) - elif message_type == "ping": - # Respond to ping with pong - ws.send(json.dumps({"type": "pong"})) - else: - self.handle_unhandled_message(message) + ``on_update`` is invoked with the full server message (both the + initial ``subscribed/...`` snapshot and subsequent ``update/...`` + messages). The callback may be sync or async. - async def on_message_async(self, ws, message): - message = json.loads(message) - message_type = message.get("type") + ``auth`` is sent alongside the subscribe message. If omitted and + ``channel`` is under one of the documented auth-required + prefixes, the client's default :attr:`auth` is used. - if message_type == "connected": - await self.handle_connected_async(ws) - elif message_type == "ping": - # Respond to ping with pong - await ws.send(json.dumps({"type": "pong"})) - else: - self.on_message(ws, message) + ``parse`` is opt-in typed payloads. When ``True``, the message is + validated through :func:`lighter.ws_messages.parse_ws_message` + and the corresponding :class:`~lighter.ws_messages.WSEnvelope` + subclass is passed to ``on_update`` instead of the raw dict. The + envelopes are forward-compatible (``extra="allow"`` + Optional + fields) so server-side schema additions do not cause parsing to + fail. + """ + canonical = _canonical_channel(channel) + self._subscriptions[canonical] = _Subscription( + channel=canonical, on_update=on_update, auth=auth, parse=parse, + ) + if self.ws is not None: + self._spawn(self._send_subscribe(canonical)) - def handle_connected(self, ws): - for market_id in self.subscriptions["order_books"]: - ws.send( - json.dumps({"type": "subscribe", "channel": f"order_book/{market_id}"}) - ) - for account_id in self.subscriptions["accounts"]: - ws.send( - json.dumps( - {"type": "subscribe", "channel": f"account_all/{account_id}"} - ) + def unsubscribe(self, channel: str) -> None: + """Unsubscribe from ``channel`` and discard any cached state.""" + canonical = _canonical_channel(channel) + self._subscriptions.pop(canonical, None) + if canonical.startswith("order_book/"): + with suppress(ValueError): + market_id = int(canonical.split("/", 1)[1]) + self.order_book_states.pop(market_id, None) + if self.ws is not None: + self._spawn( + self.send_json({"type": "unsubscribe", "channel": canonical}) ) - async def handle_connected_async(self, ws): - for market_id in self.subscriptions["order_books"]: - await ws.send( - json.dumps({"type": "subscribe", "channel": f"order_book/{market_id}"}) - ) - for account_id in self.subscriptions["accounts"]: - await ws.send( - json.dumps( - {"type": "subscribe", "channel": f"account_all/{account_id}"} + @property + def subscriptions(self) -> Mapping[str, _Subscription]: + """A read-only view of registered subscriptions.""" + return dict(self._subscriptions) + + # ------------------------------------------------------------------ + # Sending + # ------------------------------------------------------------------ + + async def send_json(self, message: Mapping[str, Any]) -> None: + """Send a JSON-encoded message on the open WebSocket connection.""" + ws = self.ws + if ws is None: + raise RuntimeError("WebSocket is not connected") + if self._send_lock is None: + self._send_lock = asyncio.Lock() + async with self._send_lock: + await ws.send(json.dumps(message)) + + async def send_tx( + self, + tx_type: int, + tx_info: Union[str, Mapping[str, Any]], + *, + id: Optional[str] = None, + ) -> None: + """Send a signed transaction via ``jsonapi/sendtx``.""" + data: Dict[str, Any] = { + "tx_type": int(tx_type), + "tx_info": _decode_tx_info(tx_info), + } + if id is not None: + data["id"] = id + await self.send_json({"type": "jsonapi/sendtx", "data": data}) + + async def send_tx_batch( + self, + tx_types: Iterable[int], + tx_infos: Iterable[Union[str, Mapping[str, Any]]], + *, + id: Optional[str] = None, + ) -> None: + """Send a batch of signed transactions via ``jsonapi/sendtxbatch``. + + ``tx_types`` and ``tx_infos`` are wire-encoded as JSON-encoded + string arrays (the form produced by the signer helpers). + """ + info_strings: List[str] = [] + for info in tx_infos: + if isinstance(info, str): + info_strings.append(info) + else: + info_strings.append(json.dumps(info)) + data: Dict[str, Any] = { + "tx_types": json.dumps([int(t) for t in tx_types]), + "tx_infos": json.dumps(info_strings), + } + if id is not None: + data["id"] = id + await self.send_json({"type": "jsonapi/sendtxbatch", "data": data}) + + # ------------------------------------------------------------------ + # Run loop + # ------------------------------------------------------------------ + + def run(self) -> None: + """Synchronous wrapper that drives :meth:`run_async` to completion.""" + asyncio.run(self.run_async()) + + async def run_async(self) -> None: + """Connect, dispatch messages, and (optionally) reconnect on errors.""" + self._stopped = False + while not self._stopped: + try: + await self._connect_and_consume() + except asyncio.CancelledError: + raise + except Exception as exc: + if not self.auto_reconnect or self._stopped: + raise + logger.warning( + "WebSocket disconnected: %s; reconnecting in %.1fs", + exc, + self.reconnect_delay, ) - ) + await asyncio.sleep(self.reconnect_delay) + continue + if not self.auto_reconnect: + return - def handle_subscribed_order_book(self, message): - market_id = message["channel"].split(":")[1] - self.order_book_states[market_id] = message["order_book"] - if self.on_order_book_update: - self.on_order_book_update(market_id, self.order_book_states[market_id]) - - def handle_update_order_book(self, message): - market_id = message["channel"].split(":")[1] - self.update_order_book_state(market_id, message["order_book"]) - if self.on_order_book_update: - self.on_order_book_update(market_id, self.order_book_states[market_id]) - - def update_order_book_state(self, market_id, order_book): - self.update_orders( - order_book["asks"], self.order_book_states[market_id]["asks"] - ) - self.update_orders( - order_book["bids"], self.order_book_states[market_id]["bids"] + async def close(self) -> None: + """Stop the run loop and close the WebSocket connection.""" + self._stopped = True + ws = self.ws + if ws is not None: + with suppress(Exception): + await ws.close() + + async def _connect_and_consume(self) -> None: + async with _ws_connect( + self.base_url, + ping_interval=self.ping_interval, + ping_timeout=self.ping_timeout, + ) as ws: + self.ws = ws + self._send_lock = asyncio.Lock() + try: + for channel in list(self._subscriptions): + await self._send_subscribe(channel) + async for raw in ws: + if isinstance(raw, bytes): + logger.debug("Ignoring binary WebSocket frame") + continue + await self._dispatch(json.loads(raw)) + finally: + self.ws = None + self._send_lock = None + + async def _send_subscribe(self, channel: str) -> None: + sub = self._subscriptions.get(channel) + if sub is None: + return + payload: Dict[str, Any] = {"type": "subscribe", "channel": channel} + token = sub.auth if sub.auth is not None else self._default_auth(channel) + if token is not None: + payload["auth"] = token + await self.send_json(payload) + + def _default_auth(self, channel: str) -> Optional[str]: + if channel.startswith(_AUTH_REQUIRED_PREFIXES): + return self.auth + return None + + def _spawn(self, coro: Coroutine[Any, Any, None]) -> None: + """Schedule a coroutine on the running event loop, if any. + + Used so the sync :meth:`subscribe` / :meth:`unsubscribe` methods can + also drive runtime subscribe/unsubscribe frames when the caller is + inside an async context. + """ + try: + loop = asyncio.get_running_loop() + except RuntimeError: + return + loop.create_task(coro) + + # ------------------------------------------------------------------ + # Message dispatch + # ------------------------------------------------------------------ + + async def _dispatch(self, message: Dict[str, Any]) -> None: + message_type = message.get("type", "") + + if message_type == "ping": + await self.send_json({"type": "pong"}) + return + if message_type == "pong": + return + if message_type.startswith("jsonapi/"): + await self._call(self.on_tx_response, message) + return + if message_type == "connected" or not message_type.startswith( + ("subscribed/", "update/") + ): + await self._call(self.on_message, message) + return + + channel_raw = message.get("channel") + if not isinstance(channel_raw, str): + await self._call(self.on_message, message) + return + channel = _canonical_channel(channel_raw) + + if channel.startswith("order_book/"): + self._update_order_book_state(channel, message) + + sub = self._subscriptions.get(channel) + if sub is None: + await self._call(self.on_message, message) + return + payload: Any = parse_ws_message(message) if sub.parse else message + await self._call(sub.on_update, payload) + + def _update_order_book_state( + self, channel: str, message: Dict[str, Any] + ) -> None: + try: + market_id = int(channel.split("/", 1)[1]) + except (IndexError, ValueError): + return + order_book = message.get("order_book") or {} + if message.get("type") == "subscribed/order_book": + self.order_book_states[market_id] = { + "asks": list(order_book.get("asks") or []), + "bids": list(order_book.get("bids") or []), + } + return + state = self.order_book_states.setdefault( + market_id, {"asks": [], "bids": []} ) + _apply_order_book_diff(order_book.get("asks") or [], state["asks"]) + _apply_order_book_diff(order_book.get("bids") or [], state["bids"]) + + async def _call( + self, callback: Optional[Callback], *args: Any + ) -> None: + if callback is None: + return + result = callback(*args) + if asyncio.iscoroutine(result): + await result + + +# --------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------- + - def update_orders(self, new_orders, existing_orders): - for new_order in new_orders: - is_new_order = True - # iterate over a copy so removal is safe - existing_order_copy = existing_orders[:] - for existing_order in existing_order_copy: - if new_order["price"] == existing_order["price"]: - is_new_order = False - existing_order["size"] = new_order["size"] - if float(new_order["size"]) == 0: - existing_orders.remove(existing_order) - - if is_new_order: - existing_orders.append(new_order) - - # final cleanup (in-place) - existing_orders[:] = [ - order for order in existing_orders if float(order["size"]) > 0 - ] - - def handle_subscribed_account(self, message): - account_id = message["channel"].split(":")[1] - self.account_states[account_id] = message - if self.on_account_update: - self.on_account_update(account_id, self.account_states[account_id]) - - def handle_update_account(self, message): - account_id = message["channel"].split(":")[1] - self.account_states[account_id] = message - if self.on_account_update: - self.on_account_update(account_id, self.account_states[account_id]) - - def handle_unhandled_message(self, message): - raise Exception(f"Unhandled message: {message}") - - def on_error(self, ws, error): - raise Exception(f"Error: {error}") - - def on_close(self, ws, close_status_code, close_msg): - raise Exception(f"Closed: {close_status_code} {close_msg}") - - def run(self): - ws = connect(self.base_url) - self.ws = ws - - for message in ws: - self.on_message(ws, message) - - async def run_async(self): - ws = await connect_async(self.base_url) - self.ws = ws - - async for message in ws: - await self.on_message_async(ws, message) +def _canonical_channel(channel: str) -> str: + """Normalize a channel string. + + Server-emitted channels use ``:`` as the separator (e.g. + ``"order_book:0"``); subscribe requests in the docs use ``/`` (e.g. + ``"order_book/0"``). The client stores and matches channels using + ``/`` everywhere so users only have to remember one form. + """ + return channel.replace(":", "/") + + +def _decode_tx_info( + tx_info: Union[str, Mapping[str, Any]], +) -> Any: + if isinstance(tx_info, str): + return json.loads(tx_info) + return dict(tx_info) + + +def _apply_order_book_diff( + new_orders: List[Dict[str, Any]], + existing_orders: List[Dict[str, Any]], +) -> None: + """Merge ``new_orders`` into ``existing_orders`` in-place using price as key. + + Entries with size ``0`` remove the corresponding price level. The + resulting list is not guaranteed to be sorted by price; consumers + that need a sorted book should sort after each update. + """ + by_price: Dict[str, Dict[str, Any]] = { + order["price"]: order for order in existing_orders + } + for new_order in new_orders: + price = new_order["price"] + try: + size = float(new_order["size"]) + except (TypeError, ValueError): + size = 0.0 + if size == 0: + by_price.pop(price, None) + else: + by_price[price] = new_order + existing_orders[:] = list(by_price.values()) diff --git a/lighter/ws_messages.py b/lighter/ws_messages.py new file mode 100644 index 0000000..fd65a4c --- /dev/null +++ b/lighter/ws_messages.py @@ -0,0 +1,306 @@ +"""Typed envelopes for Lighter WebSocket messages. + +Each server message ``type`` documented at +https://apidocs.lighter.xyz/docs/websocket-reference maps to a pydantic +envelope model below. All envelopes use ``extra="allow"`` and Optional +fields so that: + +* New fields the server may add later do not raise validation errors + (they end up in ``model_extra``). +* Missing fields show up as ``None`` instead of breaking parsing. + +The envelopes are intentionally schema-loose. They give you completion +and a stable shape to write code against without coupling tightly to the +exact field set documented at any one point in time. If you need strict +validation, set ``model_config = ConfigDict(extra="forbid")`` on a +subclass. + +Typical usage:: + + from lighter import WsClient, ws_messages + + def on_book(message: ws_messages.WSOrderBookUpdate) -> None: + for level in (message.order_book or {}).get("asks", []): + ... + + client = WsClient() + client.subscribe("order_book/0", on_update=on_book, parse=True) + client.run() + +Or standalone:: + + parsed = ws_messages.parse_ws_message(raw_dict) +""" + +from __future__ import annotations + +from typing import Any, Dict, List, Mapping, Optional, Type, Union + +from pydantic import BaseModel, ConfigDict + + +class WSEnvelope(BaseModel): + """Base envelope for all Lighter WebSocket messages. + + Accepts unknown fields (forward compatible) and treats every + channel-specific field as Optional so partial / new payloads parse + without raising. + """ + + model_config = ConfigDict(extra="allow", populate_by_name=True) + + type: str + channel: Optional[str] = None + + +class WSConnected(WSEnvelope): + """Server welcome message sent once per connection.""" + + +class WSError(WSEnvelope): + """Server-emitted error message.""" + + message: Optional[str] = None + code: Optional[int] = None + + +# --------------------------------------------------------------------- +# Public market data +# --------------------------------------------------------------------- + + +class WSOrderBookUpdate(WSEnvelope): + """``subscribed/order_book`` snapshot or ``update/order_book`` diff.""" + + order_book: Optional[Dict[str, Any]] = None + + +class WSTickerUpdate(WSEnvelope): + ticker: Optional[Dict[str, Any]] = None + + +class WSMarketStatsUpdate(WSEnvelope): + market_stats: Optional[Dict[str, Any]] = None + + +class WSSpotMarketStatsUpdate(WSEnvelope): + market_stats: Optional[Dict[str, Any]] = None + + +class WSTradeUpdate(WSEnvelope): + trades: Optional[List[Dict[str, Any]]] = None + + +class WSCandleUpdate(WSEnvelope): + candle: Optional[Dict[str, Any]] = None + resolution: Optional[str] = None + + +class WSHeightUpdate(WSEnvelope): + height: Optional[int] = None + + +# --------------------------------------------------------------------- +# Account-scoped streams +# --------------------------------------------------------------------- + + +class WSAccountAllUpdate(WSEnvelope): + """Combined account snapshot / diff from ``account_all/*``.""" + + account_id: Optional[int] = None + orders: Optional[List[Dict[str, Any]]] = None + positions: Optional[List[Dict[str, Any]]] = None + trades: Optional[List[Dict[str, Any]]] = None + funding_histories: Optional[List[Dict[str, Any]]] = None + funding_rates: Optional[List[Dict[str, Any]]] = None + shares: Optional[List[Dict[str, Any]]] = None + + +class WSAccountMarketUpdate(WSEnvelope): + account_id: Optional[int] = None + market_id: Optional[int] = None + orders: Optional[List[Dict[str, Any]]] = None + positions: Optional[List[Dict[str, Any]]] = None + trades: Optional[List[Dict[str, Any]]] = None + + +class WSAccountAllOrdersUpdate(WSEnvelope): + account_id: Optional[int] = None + orders: Optional[List[Dict[str, Any]]] = None + + +class WSAccountOrdersUpdate(WSEnvelope): + account_id: Optional[int] = None + market_id: Optional[int] = None + orders: Optional[List[Dict[str, Any]]] = None + + +class WSAccountAllTradesUpdate(WSEnvelope): + account_id: Optional[int] = None + trades: Optional[List[Dict[str, Any]]] = None + + +class WSAccountAllPositionsUpdate(WSEnvelope): + account_id: Optional[int] = None + positions: Optional[List[Dict[str, Any]]] = None + + +class WSAccountAllAssetsUpdate(WSEnvelope): + account_id: Optional[int] = None + assets: Optional[Dict[str, Dict[str, Any]]] = None + + +class WSAccountSpotAvgEntryPricesUpdate(WSEnvelope): + account_id: Optional[int] = None + avg_entry_prices: Optional[Dict[str, Any]] = None + + +class WSAccountTxUpdate(WSEnvelope): + account_id: Optional[int] = None + txs: Optional[List[Dict[str, Any]]] = None + + +class WSUserStatsUpdate(WSEnvelope): + account_id: Optional[int] = None + stats: Optional[Dict[str, Any]] = None + + +class WSNotificationUpdate(WSEnvelope): + account_id: Optional[int] = None + notifications: Optional[List[Dict[str, Any]]] = None + + +class WSPoolDataUpdate(WSEnvelope): + account_id: Optional[int] = None + + +class WSPoolInfoUpdate(WSEnvelope): + account_id: Optional[int] = None + + +# --------------------------------------------------------------------- +# Transaction submission responses +# --------------------------------------------------------------------- + + +class WSTxResponse(WSEnvelope): + """Response envelope for ``jsonapi/sendtx`` and ``jsonapi/sendtxbatch``.""" + + id: Optional[str] = None + code: Optional[int] = None + message: Optional[str] = None + tx_hash: Optional[str] = None + tx_hashes: Optional[List[str]] = None + error: Optional[Any] = None + + +# --------------------------------------------------------------------- +# Registry & dispatch helper +# --------------------------------------------------------------------- + + +_TYPE_MAP: Dict[str, Type[WSEnvelope]] = { + "connected": WSConnected, + "error": WSError, + # public market data + "subscribed/order_book": WSOrderBookUpdate, + "update/order_book": WSOrderBookUpdate, + "subscribed/ticker": WSTickerUpdate, + "update/ticker": WSTickerUpdate, + "subscribed/market_stats": WSMarketStatsUpdate, + "update/market_stats": WSMarketStatsUpdate, + "subscribed/spot_market_stats": WSSpotMarketStatsUpdate, + "update/spot_market_stats": WSSpotMarketStatsUpdate, + "subscribed/trade": WSTradeUpdate, + "update/trade": WSTradeUpdate, + "subscribed/candle": WSCandleUpdate, + "update/candle": WSCandleUpdate, + "subscribed/height": WSHeightUpdate, + "update/height": WSHeightUpdate, + # account-scoped + "subscribed/account_all": WSAccountAllUpdate, + "update/account_all": WSAccountAllUpdate, + "subscribed/account_market": WSAccountMarketUpdate, + "update/account_market": WSAccountMarketUpdate, + "subscribed/account_all_orders": WSAccountAllOrdersUpdate, + "update/account_all_orders": WSAccountAllOrdersUpdate, + "subscribed/account_orders": WSAccountOrdersUpdate, + "update/account_orders": WSAccountOrdersUpdate, + "subscribed/account_all_trades": WSAccountAllTradesUpdate, + "update/account_all_trades": WSAccountAllTradesUpdate, + "subscribed/account_all_positions": WSAccountAllPositionsUpdate, + "update/account_all_positions": WSAccountAllPositionsUpdate, + "subscribed/account_all_assets": WSAccountAllAssetsUpdate, + "update/account_all_assets": WSAccountAllAssetsUpdate, + "subscribed/account_spot_avg_entry_prices": ( + WSAccountSpotAvgEntryPricesUpdate + ), + "update/account_spot_avg_entry_prices": WSAccountSpotAvgEntryPricesUpdate, + "subscribed/account_tx": WSAccountTxUpdate, + "update/account_tx": WSAccountTxUpdate, + "subscribed/user_stats": WSUserStatsUpdate, + "update/user_stats": WSUserStatsUpdate, + "subscribed/notification": WSNotificationUpdate, + "update/notification": WSNotificationUpdate, + "subscribed/pool_data": WSPoolDataUpdate, + "update/pool_data": WSPoolDataUpdate, + "subscribed/pool_info": WSPoolInfoUpdate, + "update/pool_info": WSPoolInfoUpdate, + # transaction submission responses + "jsonapi/sendtx": WSTxResponse, + "jsonapi/sendtxbatch": WSTxResponse, +} + + +def envelope_for(message_type: str) -> Optional[Type[WSEnvelope]]: + """Return the envelope class registered for ``message_type``, or ``None``.""" + return _TYPE_MAP.get(message_type) + + +def parse_ws_message( + message: Mapping[str, Any], +) -> Union[WSEnvelope, Mapping[str, Any]]: + """Parse a raw WS message into a typed envelope. + + Returns the input ``message`` unchanged if no envelope is registered + for the message ``type`` (e.g. an unknown channel kind), so callers + can still rely on a dict-shaped fallback. + """ + msg_type = message.get("type") + if not isinstance(msg_type, str): + return message + cls = _TYPE_MAP.get(msg_type) + if cls is None: + return message + return cls.model_validate(message) + + +__all__ = [ + "WSEnvelope", + "WSConnected", + "WSError", + "WSOrderBookUpdate", + "WSTickerUpdate", + "WSMarketStatsUpdate", + "WSSpotMarketStatsUpdate", + "WSTradeUpdate", + "WSCandleUpdate", + "WSHeightUpdate", + "WSAccountAllUpdate", + "WSAccountMarketUpdate", + "WSAccountAllOrdersUpdate", + "WSAccountOrdersUpdate", + "WSAccountAllTradesUpdate", + "WSAccountAllPositionsUpdate", + "WSAccountAllAssetsUpdate", + "WSAccountSpotAvgEntryPricesUpdate", + "WSAccountTxUpdate", + "WSUserStatsUpdate", + "WSNotificationUpdate", + "WSPoolDataUpdate", + "WSPoolInfoUpdate", + "WSTxResponse", + "envelope_for", + "parse_ws_message", +] diff --git a/tools/gen_asyncapi.py b/tools/gen_asyncapi.py new file mode 100644 index 0000000..744cb13 --- /dev/null +++ b/tools/gen_asyncapi.py @@ -0,0 +1,917 @@ +"""Generate asyncapi.json for the Lighter WebSocket API. + +This script is a one-shot generator — only the produced ``asyncapi.json`` +is checked into the repo. The script lives outside the repo and is kept +in this file purely so the spec can be reproduced or extended later. + +The structure follows AsyncAPI 3.0 (https://www.asyncapi.com/docs/reference/specification/v3.0.0). +""" + +from __future__ import annotations + +import json +from typing import Any, Dict, List + + +# --------------------------------------------------------------------- +# Reused data schemas (from the "Types" section of the docs) +# --------------------------------------------------------------------- + +# All shared schemas keep ``additionalProperties: true`` so that +# server-side field additions don't require a spec bump to keep clients +# parsing successfully. Channel-specific fields are optional for the +# same reason. + +SHARED_SCHEMAS: Dict[str, Dict[str, Any]] = { + "Transaction": { + "type": "object", + "additionalProperties": True, + "description": "Transaction as emitted on the `account_tx` channel.", + "properties": { + "hash": {"type": "string"}, + "type": {"type": "integer"}, + "info": {"type": "string", "description": "JSON object encoded as string; shape depends on tx type."}, + "event_info": {"type": "string", "description": "JSON object encoded as string; shape depends on tx type."}, + "status": {"type": "integer"}, + "transaction_index": {"type": "integer"}, + "l1_address": {"type": "string"}, + "account_index": {"type": "integer"}, + "nonce": {"type": "integer"}, + "expire_at": {"type": "integer"}, + "block_height": {"type": "integer"}, + "queued_at": {"type": "integer"}, + "executed_at": {"type": "integer"}, + "sequence_index": {"type": "integer"}, + "parent_hash": {"type": "string"}, + "api_key_index": {"type": "integer"}, + "transaction_time": {"type": "integer"}, + }, + }, + "Order": { + "type": "object", + "additionalProperties": True, + "properties": { + "order_index": {"type": "integer"}, + "client_order_index": {"type": "integer"}, + "order_id": {"type": "string"}, + "client_order_id": {"type": "string"}, + "market_index": {"type": "integer"}, + "owner_account_index": {"type": "integer"}, + "initial_base_amount": {"type": "string"}, + "price": {"type": "string"}, + "nonce": {"type": "integer"}, + "remaining_base_amount": {"type": "string"}, + "is_ask": {"type": "boolean"}, + "base_size": {"type": "integer"}, + "base_price": {"type": "integer"}, + "filled_base_amount": {"type": "string"}, + "filled_quote_amount": {"type": "string"}, + "side": {"type": "string"}, + "type": { + "type": "string", + "enum": [ + "limit", + "market", + "stop-loss", + "stop-loss-limit", + "take-profit", + "take-profit-limit", + "twap", + "twap-sub", + "liquidation", + ], + }, + "time_in_force": { + "type": "string", + "enum": [ + "good-till-time", + "immediate-or-cancel", + "post-only", + "Unknown", + ], + }, + "reduce_only": {"type": "boolean"}, + "trigger_price": {"type": "string"}, + "order_expiry": {"type": "integer"}, + "status": {"type": "string"}, + "trigger_status": {"type": "string"}, + "trigger_time": {"type": "integer"}, + "parent_order_index": {"type": "integer"}, + "parent_order_id": {"type": "string"}, + "to_trigger_order_id_0": {"type": "string"}, + "to_trigger_order_id_1": {"type": "string"}, + "to_cancel_order_id_0": {"type": "string"}, + "integrator_fee_collector_index": {"type": "string"}, + "integrator_taker_fee": {"type": "string"}, + "integrator_maker_fee": {"type": "string"}, + "block_height": {"type": "integer"}, + }, + }, + "Trade": { + "type": "object", + "additionalProperties": True, + "properties": { + "trade_id": {"type": "integer"}, + "market_id": {"type": "integer"}, + "size": {"type": "string"}, + "price": {"type": "string"}, + "usd_amount": {"type": "string"}, + "ask_id": {"type": "integer"}, + "ask_account_id": {"type": "integer"}, + "bid_id": {"type": "integer"}, + "bid_account_id": {"type": "integer"}, + "is_maker_ask": {"type": "boolean"}, + "block_height": {"type": "integer"}, + "timestamp": {"type": "integer"}, + "type": {"type": "string"}, + }, + }, + "Position": { + "type": "object", + "additionalProperties": True, + "properties": { + "market_id": {"type": "integer"}, + "sign": {"type": "integer"}, + "position": {"type": "string"}, + "avg_entry_price": {"type": "string"}, + "position_value": {"type": "string"}, + "unrealized_pnl": {"type": "string"}, + "realized_pnl": {"type": "string"}, + "margin_mode": {"type": "integer"}, + "allocated_margin": {"type": "string"}, + "liquidation_price": {"type": "string"}, + }, + }, + "PoolShares": { + "type": "object", + "additionalProperties": True, + "properties": { + "pool_account_index": {"type": "integer"}, + "owner_account_index": {"type": "integer"}, + "shares_amount": {"type": "string"}, + "entry_usdc_amount": {"type": "string"}, + }, + }, + "Asset": { + "type": "object", + "additionalProperties": True, + "properties": { + "symbol": {"type": "string"}, + "asset_id": {"type": "integer"}, + "balance": {"type": "string"}, + "locked_balance": {"type": "string"}, + }, + }, + "PositionFunding": { + "type": "object", + "additionalProperties": True, + "properties": { + "timestamp": {"type": "integer"}, + "market_id": {"type": "integer"}, + "funding_id": {"type": "integer"}, + "change": {"type": "string"}, + "rate": {"type": "string"}, + "position_size": {"type": "string"}, + "position_side": {"type": "string", "enum": ["long", "short"]}, + "discount": {"type": "string"}, + }, + }, +} + + +# --------------------------------------------------------------------- +# Channels. +# +# Each entry: id, address (with {param} placeholders), parameters, +# whether subscribe requires auth, and the message-payload extras +# (fields beyond the envelope's `type` + `channel`). +# --------------------------------------------------------------------- + + +def envelope_payload(extras: Dict[str, Any]) -> Dict[str, Any]: + """Build the JSON Schema for a server message payload. + + Every message carries ``type`` and ``channel``; everything else is + channel-specific and optional. ``additionalProperties: true`` is + deliberate (see the forward-compat policy in ws_messages.py). + """ + return { + "type": "object", + "additionalProperties": True, + "required": ["type"], + "properties": { + "type": {"type": "string"}, + "channel": {"type": "string"}, + "timestamp": {"type": "integer"}, + **extras, + }, + } + + +CHANNELS: List[Dict[str, Any]] = [ + # ----- public market data ----- + { + "id": "order_book", + "address": "order_book/{market_id}", + "parameters": {"market_id": {"description": "Market index."}}, + "auth_required": False, + "title": "Order Book", + "description": "Order book snapshots and diffs for a given market. Snapshots ship on subscribe; subsequent messages are price-level diffs.", + "payload_extras": { + "order_book": { + "type": "object", + "additionalProperties": True, + "properties": { + "asks": {"type": "array", "items": {"type": "object", "additionalProperties": True}}, + "bids": {"type": "array", "items": {"type": "object", "additionalProperties": True}}, + "offset": {"type": "integer"}, + }, + }, + }, + }, + { + "id": "ticker", + "address": "ticker/{market_id}", + "parameters": {"market_id": {"description": "Market index."}}, + "auth_required": False, + "title": "Best Bid and Offer (BBO)", + "description": "Best bid/offer updates for a given market.", + "payload_extras": { + "ticker": {"type": "object", "additionalProperties": True}, + }, + }, + { + "id": "market_stats", + "address": "market_stats/{market_id}", + "parameters": { + "market_id": { + "description": "Market index, or the literal string `all` to receive stats for every market.", + }, + }, + "auth_required": False, + "title": "Market Stats", + "description": "Per-market rolling stats (volume, price change, etc.). Pass `all` as the market id to receive every market on one subscription.", + "payload_extras": { + "market_stats": {"type": "object", "additionalProperties": True}, + }, + }, + { + "id": "spot_market_stats", + "address": "spot_market_stats/{market_id}", + "parameters": { + "market_id": { + "description": "Spot market index, or the literal string `all`.", + }, + }, + "auth_required": False, + "title": "Spot Market Stats", + "description": "Per-spot-market rolling stats. Pass `all` to receive every spot market on one subscription.", + "payload_extras": { + "spot_market_stats": {"type": "object", "additionalProperties": True}, + }, + }, + { + "id": "trade", + "address": "trade/{market_id}", + "parameters": {"market_id": {"description": "Market index."}}, + "auth_required": False, + "title": "Trade", + "description": "Public trade stream for a given market.", + "payload_extras": { + "trades": { + "type": "array", + "items": {"$ref": "#/components/schemas/Trade"}, + }, + }, + }, + { + "id": "candle", + "address": "candle/{market_id}/{resolution}", + "parameters": { + "market_id": {"description": "Market index."}, + "resolution": { + "description": "Candle resolution, e.g. `1m`, `5m`, `1h`, `1d`.", + }, + }, + "auth_required": False, + "title": "Candlesticks", + "description": "Candlestick stream for a (market, resolution) pair.", + "payload_extras": { + "candle": {"type": "object", "additionalProperties": True}, + "resolution": {"type": "string"}, + }, + }, + { + "id": "height", + "address": "height", + "parameters": {}, + "auth_required": False, + "title": "Height", + "description": "Latest L2 block height.", + "payload_extras": { + "height": {"type": "integer"}, + }, + }, + # ----- account-scoped streams ----- + { + "id": "account_all", + "address": "account_all/{account_id}", + "parameters": {"account_id": {"description": "Account index."}}, + # The docs subscribe example does not show an `auth` field for + # this channel. Marked unauthenticated in the spec for parity; + # see the README note next to AUTH_REQUIRED_PREFIXES in + # ws_client.py — the auth-required list is documented to match + # the docs page exactly. + "auth_required": False, + "title": "Account All", + "description": "Combined account stream: orders, positions, trades, funding histories/rates, and pool shares.", + "payload_extras": { + "account_id": {"type": "integer"}, + "orders": {"type": "array", "items": {"$ref": "#/components/schemas/Order"}}, + "positions": {"type": "array", "items": {"$ref": "#/components/schemas/Position"}}, + "trades": {"type": "array", "items": {"$ref": "#/components/schemas/Trade"}}, + "funding_histories": {"type": "array", "items": {"$ref": "#/components/schemas/PositionFunding"}}, + "funding_rates": {"type": "array", "items": {"type": "object", "additionalProperties": True}}, + "shares": {"type": "array", "items": {"$ref": "#/components/schemas/PoolShares"}}, + }, + }, + { + "id": "account_market", + "address": "account_market/{market_id}/{account_id}", + "parameters": { + "market_id": {"description": "Market index."}, + "account_id": {"description": "Account index."}, + }, + "auth_required": True, + "title": "Account Market", + "description": "Per-market view of a specific account (orders, positions, trades restricted to one market).", + "payload_extras": { + "account_id": {"type": "integer"}, + "market_id": {"type": "integer"}, + "orders": {"type": "array", "items": {"$ref": "#/components/schemas/Order"}}, + "positions": {"type": "array", "items": {"$ref": "#/components/schemas/Position"}}, + "trades": {"type": "array", "items": {"$ref": "#/components/schemas/Trade"}}, + }, + }, + { + "id": "user_stats", + "address": "user_stats/{account_id}", + "parameters": {"account_id": {"description": "Account index."}}, + "auth_required": False, + "title": "Account Stats", + "description": "Aggregate stats for an account (collateral, portfolio value, etc.).", + "payload_extras": { + "account_id": {"type": "integer"}, + "stats": {"type": "object", "additionalProperties": True}, + }, + }, + { + "id": "account_tx", + "address": "account_tx/{account_id}", + "parameters": {"account_id": {"description": "Account index."}}, + "auth_required": True, + "title": "Account Tx", + "description": "Transaction history for a specific account.", + "payload_extras": { + "account_id": {"type": "integer"}, + "txs": { + "type": "array", + "items": {"$ref": "#/components/schemas/Transaction"}, + }, + }, + }, + { + "id": "account_all_orders", + "address": "account_all_orders/{account_id}", + "parameters": {"account_id": {"description": "Account index."}}, + "auth_required": True, + "title": "Account All Orders", + "description": "All orders across markets for an account.", + "payload_extras": { + "account_id": {"type": "integer"}, + "orders": {"type": "array", "items": {"$ref": "#/components/schemas/Order"}}, + }, + }, + { + "id": "account_orders", + "address": "account_orders/{market_id}/{account_id}", + "parameters": { + "market_id": {"description": "Market index."}, + "account_id": {"description": "Account index."}, + }, + "auth_required": True, + "title": "Account Orders", + "description": "Orders for an account scoped to a single market.", + "payload_extras": { + "account_id": {"type": "integer"}, + "market_id": {"type": "integer"}, + "orders": {"type": "array", "items": {"$ref": "#/components/schemas/Order"}}, + }, + }, + { + "id": "account_all_trades", + "address": "account_all_trades/{account_id}", + "parameters": {"account_id": {"description": "Account index."}}, + "auth_required": False, + "title": "Account All Trades", + "description": "All trades for an account across markets. Snapshot keys trades by market index; updates may emit a flat list.", + "payload_extras": { + "account_id": {"type": "integer"}, + "trades": { + "oneOf": [ + {"type": "array", "items": {"$ref": "#/components/schemas/Trade"}}, + { + "type": "object", + "additionalProperties": { + "type": "array", + "items": {"$ref": "#/components/schemas/Trade"}, + }, + }, + ], + }, + "total_volume": {"type": "number"}, + "monthly_volume": {"type": "number"}, + }, + }, + { + "id": "account_all_positions", + "address": "account_all_positions/{account_id}", + "parameters": {"account_id": {"description": "Account index."}}, + "auth_required": False, + "title": "Account All Positions", + "description": "All positions for an account, keyed by market index.", + "payload_extras": { + "account_id": {"type": "integer"}, + "positions": { + "type": "object", + "additionalProperties": {"$ref": "#/components/schemas/Position"}, + }, + }, + }, + { + "id": "account_all_assets", + "address": "account_all_assets/{account_id}", + "parameters": {"account_id": {"description": "Account index."}}, + "auth_required": True, + "title": "Account All Assets", + "description": "Per-asset balances for all spot markets for a specific account. `balance` is in coin terms, not USDC.", + "payload_extras": { + "account_id": {"type": "integer"}, + "assets": { + "type": "object", + "additionalProperties": {"$ref": "#/components/schemas/Asset"}, + }, + }, + }, + { + "id": "account_spot_avg_entry_prices", + "address": "account_spot_avg_entry_prices/{account_id}", + "parameters": {"account_id": {"description": "Account index."}}, + "auth_required": True, + "title": "Average Entry Prices", + "description": "Spot avg-entry-price stream. Each event accounts as a buy/sell at the index price; `last_trade_id` confirms the validity horizon.", + "payload_extras": { + "account_id": {"type": "integer"}, + "avg_entry_prices": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": True, + "properties": { + "asset_id": {"type": "integer"}, + "avg_entry_price": {"type": "string"}, + "asset_size": {"type": "string"}, + "last_trade_id": {"type": "integer"}, + }, + }, + }, + }, + }, + { + "id": "notification", + "address": "notification/{account_id}", + "parameters": {"account_id": {"description": "Account index."}}, + "auth_required": True, + "title": "Notification", + "description": "Per-account notification stream.", + "payload_extras": { + "account_id": {"type": "integer"}, + "notifications": { + "type": "array", + "items": {"type": "object", "additionalProperties": True}, + }, + }, + }, + { + "id": "pool_data", + "address": "pool_data/{account_id}", + "parameters": {"account_id": {"description": "Pool account index."}}, + "auth_required": True, + "title": "Pool Data", + "description": "Live data for a public pool account.", + "payload_extras": { + "account_id": {"type": "integer"}, + }, + }, + { + "id": "pool_info", + "address": "pool_info/{account_id}", + "parameters": {"account_id": {"description": "Pool account index."}}, + "auth_required": True, + "title": "Pool Info", + "description": "Public pool metadata.", + "payload_extras": { + "account_id": {"type": "integer"}, + }, + }, +] + + +# --------------------------------------------------------------------- +# Build the document +# --------------------------------------------------------------------- + + +def build_message_components() -> Dict[str, Dict[str, Any]]: + messages: Dict[str, Dict[str, Any]] = {} + # Per-channel server messages. + for ch in CHANNELS: + camel = "".join(p.title() for p in ch["id"].split("_")) + payload_schema = envelope_payload(ch["payload_extras"]) + subscribed_type = f"subscribed/{ch['id']}" + update_type = f"update/{ch['id']}" + messages[f"{camel}Subscribed"] = { + "name": f"{camel}Subscribed", + "title": f"{ch['title']} snapshot", + "summary": f"Initial snapshot delivered after a successful `subscribe` to `{ch['address']}`.", + "x-message-type": subscribed_type, + "contentType": "application/json", + "payload": payload_schema, + } + messages[f"{camel}Update"] = { + "name": f"{camel}Update", + "title": f"{ch['title']} update", + "summary": f"Live update for an existing subscription to `{ch['address']}`.", + "x-message-type": update_type, + "contentType": "application/json", + "payload": payload_schema, + } + + # Global / control-plane messages. + messages["Connected"] = { + "name": "Connected", + "title": "Connection welcome", + "summary": "Sent once when the WebSocket connection is established.", + "x-message-type": "connected", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": True, + "required": ["type"], + "properties": {"type": {"const": "connected"}}, + }, + } + messages["ServerError"] = { + "name": "ServerError", + "title": "Server error", + "summary": "Server-emitted error frame.", + "x-message-type": "error", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": True, + "required": ["type"], + "properties": { + "type": {"const": "error"}, + "message": {"type": "string"}, + "code": {"type": "integer"}, + }, + }, + } + messages["Pong"] = { + "name": "Pong", + "title": "Application-level pong", + "summary": "Reply to a client-sent `ping` frame.", + "x-message-type": "pong", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": True, + "required": ["type"], + "properties": {"type": {"const": "pong"}}, + }, + } + messages["TxResponse"] = { + "name": "TxResponse", + "title": "Transaction submission response", + "summary": "Reply to `jsonapi/sendtx` or `jsonapi/sendtxbatch`.", + "x-message-type": "jsonapi/sendtx", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": True, + "required": ["type"], + "properties": { + "type": {"type": "string"}, + "id": {"type": "string"}, + "code": {"type": "integer"}, + "message": {"type": "string"}, + "tx_hash": {"type": "string"}, + "tx_hashes": {"type": "array", "items": {"type": "string"}}, + "error": {}, + }, + }, + } + + # Client → server messages. + messages["SubscribeRequest"] = { + "name": "SubscribeRequest", + "title": "Subscribe", + "summary": "Open a subscription on a channel.", + "x-message-type": "subscribe", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": True, + "required": ["type", "channel"], + "properties": { + "type": {"const": "subscribe"}, + "channel": { + "type": "string", + "description": "Channel address. Use `/` as the path separator (e.g. `order_book/0`).", + }, + "auth": { + "type": "string", + "description": "Bearer token. Required for the channels listed under `securitySchemes.bearerToken`.", + }, + }, + }, + } + messages["UnsubscribeRequest"] = { + "name": "UnsubscribeRequest", + "title": "Unsubscribe", + "summary": "Cancel an existing subscription.", + "x-message-type": "unsubscribe", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": True, + "required": ["type", "channel"], + "properties": { + "type": {"const": "unsubscribe"}, + "channel": {"type": "string"}, + }, + }, + } + messages["Ping"] = { + "name": "Ping", + "title": "Application-level ping", + "summary": "Heartbeat frame. The server replies with a `Pong`. Either WebSocket-level ping frames or this application-level frame satisfy the 2-minute idle requirement.", + "x-message-type": "ping", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": True, + "required": ["type"], + "properties": {"type": {"const": "ping"}}, + }, + } + messages["SendTx"] = { + "name": "SendTx", + "title": "Send transaction", + "summary": "Submit a single signed transaction over the socket.", + "x-message-type": "jsonapi/sendtx", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": True, + "required": ["type", "data"], + "properties": { + "type": {"const": "jsonapi/sendtx"}, + "data": { + "type": "object", + "additionalProperties": True, + "required": ["tx_type", "tx_info"], + "properties": { + "tx_type": {"type": "integer"}, + "tx_info": { + "description": "Signed payload produced by SignerClient. Usually a JSON-encoded string.", + }, + }, + }, + }, + }, + } + messages["SendTxBatch"] = { + "name": "SendTxBatch", + "title": "Send transaction batch", + "summary": "Submit up to 15 signed transactions in one message. `tx_infos` is a JSON-encoded list of JSON-encoded `tx_info` strings (double-encoded).", + "x-message-type": "jsonapi/sendtxbatch", + "contentType": "application/json", + "payload": { + "type": "object", + "additionalProperties": True, + "required": ["type", "data"], + "properties": { + "type": {"const": "jsonapi/sendtxbatch"}, + "data": { + "type": "object", + "additionalProperties": True, + "required": ["tx_types", "tx_infos"], + "properties": { + "tx_types": { + "type": "string", + "description": "JSON-encoded list of integer tx types, e.g. `\"[14,14]\"`.", + }, + "tx_infos": { + "type": "string", + "description": "JSON-encoded list of JSON-encoded tx_info strings.", + }, + }, + }, + }, + }, + } + + return messages + + +def build_channels() -> Dict[str, Dict[str, Any]]: + channels: Dict[str, Dict[str, Any]] = {} + for ch in CHANNELS: + camel = "".join(p.title() for p in ch["id"].split("_")) + params = { + name: {"description": spec["description"]} + for name, spec in ch["parameters"].items() + } + ch_doc: Dict[str, Any] = { + "address": ch["address"], + "title": ch["title"], + "description": ch["description"], + "messages": { + f"{camel}Subscribed": {"$ref": f"#/components/messages/{camel}Subscribed"}, + f"{camel}Update": {"$ref": f"#/components/messages/{camel}Update"}, + }, + } + if params: + ch_doc["parameters"] = params + channels[ch["id"]] = ch_doc + + # Single control-plane channel for everything that isn't tied to a + # subscription address (the WebSocket itself). + channels["_control"] = { + "address": "(connection)", + "title": "Connection control plane", + "description": "Frames not tied to a subscription address: client → server `subscribe`/`unsubscribe`/`ping`/`jsonapi/sendtx`/`jsonapi/sendtxbatch`, and server → client `connected`/`error`/`pong`/`jsonapi/*` responses.", + "messages": { + "SubscribeRequest": {"$ref": "#/components/messages/SubscribeRequest"}, + "UnsubscribeRequest": {"$ref": "#/components/messages/UnsubscribeRequest"}, + "Ping": {"$ref": "#/components/messages/Ping"}, + "SendTx": {"$ref": "#/components/messages/SendTx"}, + "SendTxBatch": {"$ref": "#/components/messages/SendTxBatch"}, + "Connected": {"$ref": "#/components/messages/Connected"}, + "ServerError": {"$ref": "#/components/messages/ServerError"}, + "Pong": {"$ref": "#/components/messages/Pong"}, + "TxResponse": {"$ref": "#/components/messages/TxResponse"}, + }, + } + return channels + + +def build_operations() -> Dict[str, Dict[str, Any]]: + operations: Dict[str, Dict[str, Any]] = {} + for ch in CHANNELS: + camel = "".join(p.title() for p in ch["id"].split("_")) + sub_op: Dict[str, Any] = { + "action": "send", + "channel": {"$ref": f"#/channels/{ch['id']}"}, + "summary": f"Subscribe to `{ch['address']}`.", + "messages": [{"$ref": "#/components/messages/SubscribeRequest"}], + } + if ch["auth_required"]: + sub_op["security"] = [{"$ref": "#/components/securitySchemes/bearerToken"}] + operations[f"subscribe_{ch['id']}"] = sub_op + + operations[f"receive_{ch['id']}"] = { + "action": "receive", + "channel": {"$ref": f"#/channels/{ch['id']}"}, + "summary": f"Receive snapshot + updates for `{ch['address']}`.", + "messages": [ + {"$ref": f"#/channels/{ch['id']}/messages/{camel}Subscribed"}, + {"$ref": f"#/channels/{ch['id']}/messages/{camel}Update"}, + ], + } + + # Control-plane operations. + operations["unsubscribe"] = { + "action": "send", + "channel": {"$ref": "#/channels/_control"}, + "summary": "Cancel an existing subscription.", + "messages": [{"$ref": "#/components/messages/UnsubscribeRequest"}], + } + operations["ping"] = { + "action": "send", + "channel": {"$ref": "#/channels/_control"}, + "summary": "Application-level heartbeat (server replies with `Pong`).", + "messages": [{"$ref": "#/components/messages/Ping"}], + } + operations["send_tx"] = { + "action": "send", + "channel": {"$ref": "#/channels/_control"}, + "summary": "Submit a single signed transaction.", + "messages": [{"$ref": "#/components/messages/SendTx"}], + } + operations["send_tx_batch"] = { + "action": "send", + "channel": {"$ref": "#/channels/_control"}, + "summary": "Submit up to 15 signed transactions in a single frame.", + "messages": [{"$ref": "#/components/messages/SendTxBatch"}], + } + operations["receive_connected"] = { + "action": "receive", + "channel": {"$ref": "#/channels/_control"}, + "summary": "Connection welcome frame.", + "messages": [{"$ref": "#/channels/_control/messages/Connected"}], + } + operations["receive_error"] = { + "action": "receive", + "channel": {"$ref": "#/channels/_control"}, + "summary": "Server error frame.", + "messages": [{"$ref": "#/channels/_control/messages/ServerError"}], + } + operations["receive_pong"] = { + "action": "receive", + "channel": {"$ref": "#/channels/_control"}, + "summary": "Reply to a client `Ping`.", + "messages": [{"$ref": "#/channels/_control/messages/Pong"}], + } + operations["receive_tx_response"] = { + "action": "receive", + "channel": {"$ref": "#/channels/_control"}, + "summary": "Reply to `jsonapi/sendtx` or `jsonapi/sendtxbatch` (success or error).", + "messages": [{"$ref": "#/channels/_control/messages/TxResponse"}], + } + return operations + + +def build_document() -> Dict[str, Any]: + return { + "asyncapi": "3.0.0", + "info": { + "title": "Lighter WebSocket API", + "version": "1.0.0", + "description": ( + "Real-time market data, account state, and transaction submission " + "for the zkLighter exchange. Hand-mirrored from " + "https://apidocs.lighter.xyz/docs/websocket-reference. The schemas " + "are intentionally permissive (`additionalProperties: true`, all " + "channel-specific fields optional) so server-side additions do " + "not invalidate generated clients." + ), + "contact": { + "name": "Lighter API docs", + "url": "https://apidocs.lighter.xyz/docs/websocket-reference", + }, + }, + "defaultContentType": "application/json", + "servers": { + "mainnet": { + "host": "mainnet.zklighter.elliot.ai", + "pathname": "/stream", + "protocol": "wss", + "description": "Mainnet WebSocket gateway. Append `?readonly=true` to bypass IP region restrictions for read-only data.", + }, + "testnet": { + "host": "testnet.zklighter.elliot.ai", + "pathname": "/stream", + "protocol": "wss", + "description": "Testnet WebSocket gateway.", + }, + }, + "channels": build_channels(), + "operations": build_operations(), + "components": { + "messages": build_message_components(), + "schemas": SHARED_SCHEMAS, + "securitySchemes": { + "bearerToken": { + "type": "http", + "scheme": "bearer", + "description": ( + "Per-channel auth token passed in the `auth` field of the `subscribe` " + "message. Required for channels whose subscribe operation lists this " + "scheme under `security`. See the `apikeys` REST endpoint for token " + "generation." + ), + }, + }, + }, + } + + +if __name__ == "__main__": + import sys + + out = build_document() + json.dump(out, sys.stdout, indent=2, sort_keys=False) + sys.stdout.write("\n")