Skip to content

Rewrite ws_client.py against the documented WebSocket API#153

Open
devin-ai-integration[bot] wants to merge 3 commits into
mainfrom
devin/1779104618-rewrite-ws-client
Open

Rewrite ws_client.py against the documented WebSocket API#153
devin-ai-integration[bot] wants to merge 3 commits into
mainfrom
devin/1779104618-rewrite-ws-client

Conversation

@devin-ai-integration
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot commented May 18, 2026

Summary

Rewrites lighter/ws_client.py against the channels documented at https://apidocs.lighter.xyz/docs/websocket-reference. The previous client only handled order_book and account_all; the new one covers every documented channel, adds first-class transaction submission over the socket, exposes a single uniform subscription API, and ships opt-in typed payload envelopes.

Unified subscribe() API (breaking)

The 20+ per-channel constructor params and on_*_update callbacks are gone. All channels go through one method:

client = lighter.WsClient(auth=...)
client.subscribe("order_book/0", on_update=on_book)
client.subscribe("trade/0", on_update=on_trades)
client.subscribe("account_tx/12345", on_update=on_tx)  # auth auto-attached
client.run()
  • Callbacks receive the full server message (dict) and may be sync or async.
  • Auth is auto-attached for channels under the documented auth-required prefixes (account_market/, account_tx/, account_all_orders/, account_orders/, account_all_assets/, account_spot_avg_entry_prices/, notification/, pool_data/, pool_info/). Per-subscription tokens override the client default.
  • unsubscribe(channel) works at runtime; subscribe() called after run() dispatches the subscribe frame on the live connection.
  • order_book/* snapshots and diffs are still merged into client.order_book_states[market_id] for callers who want a maintained book.

Opt-in typed envelopes (lighter/ws_messages.py)

A new module exposes one pydantic envelope per documented server message type (WSOrderBookUpdate, WSTradeUpdate, WSCandleUpdate, WSAccountAllUpdate, WSTxResponse, …). Every envelope uses ConfigDict(extra="allow") and treats channel-specific fields as Optional, so:

  • New server fields land in model_extra and do not raise.
  • Removed/missing fields surface as None rather than breaking parsing.
def on_book(msg: ws_messages.WSOrderBookUpdate) -> None:
    asks = (msg.order_book or {}).get("asks", [])

client.subscribe("order_book/0", on_update=on_book, parse=True)

parse_ws_message(raw_dict) and envelope_for(message_type) are also exported for use outside the client (replay tooling, tests). Unknown message types fall through and return the raw dict, so adding new server channels never makes typed mode fatal.

Other changes

  • send_tx / send_tx_batch wrap jsonapi/sendtx / jsonapi/sendtxbatch. The batch encoding matches the existing examples/utils.py:ws_send_batch_tx wire format (tx_infos as a JSON-encoded list of JSON-encoded strings).
  • ?readonly=true query option, configurable WebSocket-level ping_interval/ping_timeout (default 30s/60s to stay below the server's 2-minute idle timeout), application-level pingpong reply.
  • Optional auto_reconnect with reconnect_delay; registered subscriptions are re-sent on each reconnect.
  • close() for graceful shutdown.
  • Channel-name parsing accepts both : (server) and / (subscribe) separators and normalizes to / internally.
  • Order book diff rewritten using a price-indexed dict — equivalent behavior (zero-size removes the level) but the result list is no longer guaranteed sorted by price.

Breaking API changes (vs. previous ws_client.py)

  • Removed: order_book_ids, account_ids, on_order_book_update, on_account_update constructor params.
  • Removed: per-channel on_*_update style callbacks; everything is on_update(message) on each subscribe.
  • Removed: implicit error when no IDs are passed; constructing a client with zero subscriptions is now valid.
  • client.subscriptions is now a dict[channel_str, Optional[auth_token]] (was {"order_books": [...], "accounts": [...]}).
  • examples/ws.py and examples/ws_async.py have been rewritten against the new API.

Review & Testing Checklist for Human

  • Verify the basic flow against testnet/mainnet by running examples/ws.py and examples/ws_async.py: confirm both order book and account streams emit.
  • Subscribe to an auth-only channel (e.g. account_tx/<account_id> or notification/<account_id>) with a real token and confirm the server returns subscribed/... rather than an auth error. The auth-required prefix list in ws_client.py was assembled from the docs and has not been validated against server behavior.
  • Exercise send_tx and send_tx_batch with real signed payloads — the batch form preserves the double-JSON-encoded shape from examples/utils.py, which the docs describe ambiguously.
  • Spot-check parse=True for a couple of channels you care about — the envelopes were hand-mirrored from the docs, so any field-name drift between docs and live server payloads will surface as None (silent) rather than as a validation error. If you'd rather have loud failures, swap a subclass to ConfigDict(extra="forbid") and remove Optional.
  • Confirm no downstream code depends on the removed order_book_ids / account_ids constructor params, the old on_*_update signatures, or the previous client.subscriptions shape.

Notes

  • on_message is the fallthrough for any server message that doesn't match a registered subscription (including the connected welcome). on_tx_response fires for every jsonapi/* reply (success or error). Both still receive raw dicts; only subscribe(..., parse=True) triggers envelope parsing.
  • Pre-existing mypy/lint errors in the repo are unchanged. The new try/except ImportError for websockets.asyncio.client vs. websockets.client matches the same pattern in lighter/paper_client/live.py.
  • I did not run the client against the live WebSocket server in this session — please verify on testnet before merging.

Link to Devin session: https://app.devin.ai/sessions/5169092926f142a5bfec4b97315b935c
Requested by: @lavrric

Replaces the previous order-book/account-only client with a full
implementation of the channels documented at
https://apidocs.lighter.xyz/docs/websocket-reference.

The new WsClient supports every documented channel (order book,
ticker, market_stats, spot_market_stats, trade, candle, all
account_* streams, user_stats, notification, pool_data, pool_info,
height) plus jsonapi/sendtx and jsonapi/sendtxbatch transaction
submission. It keeps the existing public surface used in
examples/ws.py and examples/ws_async.py (order_book_ids,
account_ids, on_order_book_update, on_account_update, run(),
run_async()) and adds add_* helpers, async subscribe/unsubscribe,
send_tx/send_tx_batch, readonly mode, ping/pong handling,
configurable WebSocket keepalive, and optional auto-reconnect.
@devin-ai-integration
Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

Replaces the 20+ on_X_update constructor params and add_X helper methods
with one uniform subscribe(channel, on_update=..., auth=...) entry point.
Dispatch looks up the registered subscription by canonical channel name
(channels are normalized so callers can pass either ':' or '/' separators).

Order book snapshot+diff reconstruction is still performed automatically
for any 'order_book/*' subscription and exposed via client.order_book_states[market_id].

Other changes:
- subscribe/unsubscribe are sync registration; if called while connected
  they also send the subscribe/unsubscribe frame via the running loop.
- order_book_ids / account_ids constructor shortcuts and the per-channel
  on_*_update callbacks are removed. examples/ws.py and examples/ws_async.py
  are rewritten against the new API.
- on_message is now the fallback for the 'connected' welcome and any
  unmatched channels. on_tx_response fires for every 'jsonapi/*' reply.
Introduces lighter/ws_messages.py with one pydantic envelope per documented
server message type (WSOrderBookUpdate, WSTradeUpdate, WSCandleUpdate,
WSAccountAllUpdate, etc., plus WSTxResponse for jsonapi/* replies).

Every envelope uses ConfigDict(extra='allow') and treats channel-specific
fields as Optional, so new server fields land in model_extra and missing
fields surface as None - schema drift never causes parsing to raise.

WsClient.subscribe gains a parse=False flag. When True, the dispatcher
runs the message through ws_messages.parse_ws_message before invoking
on_update, so callbacks receive a typed envelope instead of a dict.
on_message and on_tx_response still receive the raw dict.

ws_messages also exposes envelope_for(message_type) and a standalone
parse_ws_message(message) helper for users who want to validate messages
outside the run loop.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant