Skip to main content

bulk_client/api/
bulk_ws.rs

1//! Bulk Labs WebSocket Trading Client — Actor + Watch architecture.
2//!
3//! All mutable state lives inside a single [`Actor`] task. The public
4//! [`BulkWsClient`] handle is a cheap, cloneable struct that communicates
5//! with the actor via:
6//!
7//! - **`tokio::sync::watch`** for hot-path reads (tickers, prices, margin)
8//!   — zero-cost `.borrow()`, no async round-trip.
9//! - **`mpsc` command channel** for writes (subscribe, place orders, etc.).
10//! - **`oneshot`** for request/response flows (order placement).
11//!
12//! ```text
13//!  ┌──────────────┐         mpsc::channel           ┌───────────────┐
14//!  │ BulkWsClient │ ───── Command ────────────────▶ │     Actor     │
15//!  │   (handle)   │ ◀──── watch::Receiver ────────  │  (owns state) │
16//!  └──────────────┘                                 └───────┬───────┘
17//!        │                                                  │
18//!        │ oneshot for order responses                      │ tokio::select!
19//!        └─────────────────────────────────────────────────▶│◀── ws_read
20//! ```
21//!
22//! # Example
23//!
24//! ```rust,no_run
25//! use bulk_client::*;
26//! use bulk_client::common::side::Side;
27//! use bulk_client::common::tif::TimeInForce;
28//! use bulk_client::transaction::TransactionSigner;
29//! use bulk_client::parts::WSConfig;
30//!
31//! #[tokio::main]
32//! async fn main() -> eyre::Result<()> {
33//!     let signer = TransactionSigner::from_private_key("your_base58_key")?;
34//!
35//!     let client = BulkWsClient::connect(WSConfig {
36//!         url: "wss://exchange-wss.bulk.trade".into(),
37//!         symbols: vec!["BTC-USD".into(), "ETH-USD".into()],
38//!         signer: Some(signer),
39//!         ..Default::default()
40//!     }).await?;
41//!
42//!     // Zero-cost read — no lock, no channel round-trip
43//!     if let Some(ticker) = client.get_ticker("BTC-USD") {
44//!         println!("BTC mark price: {}", ticker.mark_price);
45//!     }
46//!
47//!     // Place an order — goes through actor → ws
48//!     let resp = client.place_limit_order(
49//!         "BTC-USD", Side::Buy, 95_000.0, 0.01,
50//!         TimeInForce::GTC, false, None, None,
51//!     ).await?;
52//!
53//!     client.shutdown().await;
54//!     Ok(())
55//! }
56//! ```
57
58// ─────────────────────────────────────────────────────────────────────────────
59// Topic enum (mirrors topics.py)
60// ─────────────────────────────────────────────────────────────────────────────
61
62use crate::msgs::account::{
63    CommissionApproval, Fill, LeverageSetting, Margin, OrderState, PositionInfo,
64};
65use crate::msgs::responses::Response;
66use crate::msgs::subscription::SubscriptionRequest;
67use eyre::bail;
68use serde_json::{json, Value};
69use std::collections::HashMap;
70use std::str::FromStr;
71use std::sync::atomic::{AtomicBool, Ordering};
72use std::sync::{Arc, Mutex};
73use std::time::Duration;
74
75use crate::api::parts::command::Command;
76use crate::api::parts::config::WSConfig;
77use crate::api::parts::{make_nonce, Event, Topic};
78use crate::common::side::Side;
79use crate::common::tif::TimeInForce;
80use crate::msgs::md::{Candle, L2Snapshot, Ticker};
81use crate::msgs::{
82    ApproveCommissionFee, CancelAll, CancelOrder, LimitOrder, MarketOrder, Price,
83    RevokeCommissionFee,
84};
85use crate::transaction::{Action, ActionMeta, Transaction, TransactionSigner};
86use futures_util::stream::SplitSink;
87use futures_util::{SinkExt, StreamExt};
88use serde::Deserialize;
89use solana_hash::Hash;
90use solana_pubkey::Pubkey;
91use tokio::net::TcpStream;
92use tokio::sync::{broadcast, mpsc, oneshot, watch};
93use tokio::time;
94use tokio_tungstenite::{connect_async, tungstenite::Message, MaybeTlsStream, WebSocketStream};
95use tracing::{debug, error, info, warn};
96// ─────────────────────────────────────────────────────────────────────────────
97// Snapshot: the full state picture pushed over a single watch channel
98// ─────────────────────────────────────────────────────────────────────────────
99
100/// Everything a reader might want, exposed as one cheap clone via `watch`.
101/// The actor publishes a new snapshot after every state-mutating message.
102#[derive(Debug, Clone, Default, Deserialize)]
103#[allow(unused)]
104pub struct AccountState {
105    pub margin: Margin,
106    pub positions: HashMap<String, PositionInfo>,
107    pub open_orders: HashMap<String, OrderState>,
108    pub leverage_settings: HashMap<String, LeverageSetting>,
109    pub commission_approvals: Vec<CommissionApproval>,
110}
111
112// ═════════════════════════════════════════════════════════════════════════════
113// Event callback
114// ═════════════════════════════════════════════════════════════════════════════
115
116/// User-supplied callback. Receives the raw JSON payload for the topic.
117/// Runs synchronously inside the actor loop — keep it fast or spawn.
118#[allow(unused)]
119pub type EventHandler = Box<dyn Fn(&Event) + Send + Sync>;
120
121// ═════════════════════════════════════════════════════════════════════════════
122// BulkWsClient  —  the public handle (cheap clone, no locks)
123// ═════════════════════════════════════════════════════════════════════════════
124
125/// Cloneable client handle.
126///
127/// - **Hot reads** (ticker, price, margin): `watch::Receiver::borrow()` — zero
128///   async overhead, just a ref-counted pointer swap.
129/// - **Writes** (subscribe, place orders): go through the `mpsc` command
130///   channel to the actor, which serializes all mutations.
131/// - **Cold reads** (open orders list): round-trip through the actor via
132///   `oneshot` — still fast, but async.
133#[allow(unused)]
134#[derive(Clone)]
135pub struct BulkWsClient {
136    // Command channel to the actor
137    cmd_tx: mpsc::Sender<Command>,
138    // Event handlers
139    handlers: Arc<Mutex<HashMap<Topic, Vec<EventHandler>>>>,
140
141    // ── Watch receivers (hot-path, lock-free) ──────────────────────────
142    /// Per-symbol ticker snapshots.
143    ticker_rx: watch::Receiver<HashMap<String, Ticker>>,
144    /// Consolidated account state (margin, positions, orders, leverage).
145    account_rx: watch::Receiver<AccountState>,
146
147    // ── Config carried on the handle for convenience ───────────────────
148    signer: Option<TransactionSigner>,
149    default_timeout: Duration,
150
151    // Monotonic request ID (atomic — no lock needed)
152    next_request_id: std::sync::Arc<std::sync::atomic::AtomicU64>,
153
154    // Actor join handle (held in Arc so Clone works)
155    actor_handle: std::sync::Arc<tokio::sync::Mutex<Option<tokio::task::JoinHandle<()>>>>,
156
157    /// `true` while the WebSocket actor is running.  Flipped to `false` the
158    /// moment the actor begins disconnect teardown.  Cheap sync check, no await.
159    connected: Arc<AtomicBool>,
160
161    /// Fires once (with the disconnect reason) when the actor exits.
162    /// Clone a receiver via [`BulkWsClient::subscribe_disconnect`] so that
163    /// any spawned task can unblock immediately on connection loss.
164    disconnect_tx: broadcast::Sender<String>,
165}
166
167#[allow(unused)]
168impl BulkWsClient {
169    // ─────────────────────────────────────────────────────────────────────
170    // Construction + connection
171    // ─────────────────────────────────────────────────────────────────────
172
173    /// Connect to the exchange and spawn the actor task.
174    /// Returns immediately once the WebSocket handshake succeeds.
175    ///
176    /// # Arguments
177    /// - `config`: web socket config
178    pub async fn connect(config: WSConfig) -> eyre::Result<Self> {
179        info!("Connecting to {}", config.url);
180        let (ws_stream, _) = connect_async(&config.url).await?;
181        let (ws_write, ws_read) = ws_stream.split();
182        info!("Connected to Bulk Exchange WebSocket");
183
184        // Watch channels
185        let (ticker_tx, ticker_rx) = watch::channel(HashMap::new());
186        let (account_tx, account_rx) = watch::channel(AccountState::default());
187
188        // Command channel (bounded — back-pressure if actor falls behind)
189        let (cmd_tx, cmd_rx) = mpsc::channel::<Command>(512);
190
191        // Shared handler map between the dispatch task and BulkWsClient (for on() registration)
192        let handlers: Arc<Mutex<HashMap<Topic, Vec<EventHandler>>>> = Arc::default();
193        let handlers_task = Arc::clone(&handlers);
194        let (event_tx, mut event_rx) = mpsc::channel::<(Topic, Event)>(32768);
195
196        tokio::spawn(async move {
197            while let Some((topic, event)) = event_rx.recv().await {
198                let map = handlers_task.lock().unwrap();
199                if let Some(hs) = map.get(&topic) {
200                    for h in hs {
201                        h(&event);
202                    }
203                }
204            }
205        });
206
207        // Build actor
208        let connected = Arc::new(AtomicBool::new(true));
209        let (disconnect_tx, _) = broadcast::channel::<String>(4);
210
211        let actor = Actor {
212            ws_write,
213            event_tx,
214            cmd_rx,
215            ticker_tx,
216            account_tx,
217            tickers: HashMap::new(),
218            prices: HashMap::new(),
219            account_state: AccountState::default(),
220            pending: HashMap::new(),
221            subscriptions: Vec::new(),
222            connected: Arc::clone(&connected),
223            disconnect_tx: disconnect_tx.clone(),
224        };
225
226        // Default subscriptions (same as Python __init__)
227        let mut initial_subs = Vec::new();
228        if config.track_account {
229            if let Some(ref signer) = config.signer {
230                let pk_str = signer.public_key_b58();
231                initial_subs.push(SubscriptionRequest::new(
232                    "account",
233                    json!({ "user": pk_str }),
234                ));
235            }
236        }
237        if config.track_ticker {
238            for sym in &config.symbols {
239                initial_subs.push(SubscriptionRequest::new("ticker", json!({ "symbol": sym })));
240            }
241        }
242
243        // Spawn actor
244        let actor_handle = tokio::spawn(actor.run(ws_read, initial_subs));
245
246        Ok(Self {
247            cmd_tx,
248            handlers,
249            ticker_rx,
250            account_rx,
251            signer: config.signer,
252            default_timeout: config.default_timeout,
253            next_request_id: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(1)),
254            actor_handle: std::sync::Arc::new(tokio::sync::Mutex::new(Some(actor_handle))),
255            connected,
256            disconnect_tx,
257        })
258    }
259
260    /// Shut down the actor and close the WebSocket.
261    pub async fn shutdown(&self) {
262        let _ = self.cmd_tx.send(Command::Shutdown).await;
263        if let Some(h) = self.actor_handle.lock().await.take() {
264            let _ = h.await;
265        }
266    }
267
268    /// Wait for the actor to exit (e.g. on disconnect / error).
269    pub async fn closed(&self) {
270        if let Some(h) = self.actor_handle.lock().await.take() {
271            let _ = h.await;
272        }
273    }
274
275    /// Returns `true` if the WebSocket actor is still running.
276    ///
277    /// This is a cheap, lock-free check — safe to call in hot loops.
278    /// Once it returns `false` you must call [`BulkWsClient::connect`] again
279    /// to establish a new connection.
280    pub fn is_connected(&self) -> bool {
281        self.connected.load(Ordering::Relaxed)
282    }
283
284    // ─────────────────────────────────────────────────────────────────────
285    // Hot-path reads (zero-cost — no async, no lock)
286    // ─────────────────────────────────────────────────────────────────────
287
288    /// Get latest ticker for `symbol`, or `None` if not yet received.
289    ///
290    /// # Arguments
291    /// - `symbol`: symbol to retrieve for
292    ///
293    /// # Returns
294    /// - current ticker if available
295    pub fn get_ticker(&self, symbol: &str) -> Option<Ticker> {
296        self.ticker_rx.borrow().get(symbol).cloned()
297    }
298
299    /// Get Latest mark price for `symbol`.
300    ///
301    /// # Arguments
302    /// - `symbol`: symbol to retrieve for
303    ///
304    /// # Returns
305    /// - current price if available
306    pub fn get_price(&self, symbol: &str) -> Option<f64> {
307        self.ticker_rx.borrow().get(symbol).map(|x| x.mark_price)
308    }
309
310    /// All current tickers, keyed by symbol.
311    ///
312    /// # Returns
313    /// - all current tickers
314    pub fn get_tickers(&self) -> HashMap<String, Ticker> {
315        self.ticker_rx.borrow().clone()
316    }
317
318    /// Get Current account margin.
319    pub fn get_margin(&self) -> Margin {
320        self.account_rx.borrow().margin.clone()
321    }
322
323    /// Get current position for `symbol`.
324    ///
325    /// # Arguments
326    /// - `symbol`: symbol to retrieve for
327    ///
328    /// # Returns
329    /// - current position for symbol if available
330    pub fn get_position(&self, symbol: &str) -> Option<PositionInfo> {
331        self.account_rx.borrow().positions.get(symbol).cloned()
332    }
333
334    /// Get all positions.
335    pub fn get_positions(&self) -> HashMap<String, PositionInfo> {
336        self.account_rx.borrow().positions.clone()
337    }
338
339    /// Current leverage setting for `symbol`.
340    ///
341    /// # Arguments
342    /// - `symbol`: symbol to retrieve for
343    ///
344    /// # Returns
345    /// - current leverage if available
346    pub fn get_leverage(&self, symbol: &str) -> Option<f64> {
347        self.account_rx
348            .borrow()
349            .leverage_settings
350            .get(symbol)
351            .map(|l| l.leverage)
352    }
353
354    // ─────────────────────────────────────────────────────────────────────
355    // Async reads that wait for changes
356    // ─────────────────────────────────────────────────────────────────────
357
358    /// Block until any ticker changes, then return the updated map.
359    pub async fn wait_tickers_changed(&mut self) -> eyre::Result<HashMap<String, Ticker>> {
360        self.ticker_rx.changed().await?;
361        Ok(self.ticker_rx.borrow().clone())
362    }
363
364    /// Block until account state changes (margin, positions, orders, leverage).
365    pub async fn wait_account_changed(&mut self) -> eyre::Result<AccountState> {
366        self.account_rx.changed().await?;
367        Ok(self.account_rx.borrow().clone())
368    }
369
370    // ─────────────────────────────────────────────────────────────────────
371    // Cold reads (round-trip through actor)
372    // ─────────────────────────────────────────────────────────────────────
373
374    /// Open orders, optionally filtered by symbol.
375    ///
376    /// # Arguments
377    /// - `symbol`: optional symbol to retrieve for
378    ///
379    /// # Returns
380    /// - current orders or order status
381    pub async fn open_orders(&self, symbol: Option<&str>) -> eyre::Result<Vec<OrderState>> {
382        let (tx, rx) = oneshot::channel();
383        self.cmd_tx
384            .send(Command::GetOrders {
385                symbol: symbol.map(Into::into),
386                respond: tx,
387            })
388            .await
389            .map_err(|_| eyre::eyre!("actor gone"))?;
390        Ok(rx.await?)
391    }
392
393    // ─────────────────────────────────────────────────────────────────────
394    // Order placement
395    // ─────────────────────────────────────────────────────────────────────
396
397    /// Place one or more actions (limit, market, cancel, cancel-all).
398    /// Signs the bundle, sends through the actor, and awaits the exchange
399    /// response with the configured timeout.
400    ///
401    /// # Arguments
402    /// - `actions`: list of orders, cancels, etc
403    /// - `nonce`: nonce to be used
404    ///
405    /// # Returns
406    /// - list of responses
407    pub async fn place_orders(
408        &self,
409        actions: Vec<Action>,
410        account: Option<Pubkey>,
411        nonce: Option<u64>,
412    ) -> eyre::Result<Vec<Response>> {
413        let signer = self
414            .signer
415            .as_ref()
416            .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
417
418        let account = if let Some(account) = account {
419            account
420        } else {
421            signer.public_key()
422        };
423
424        let nonce = nonce.unwrap_or_else(make_nonce);
425        let pk = signer.public_key();
426
427        // Build + sign the transaction
428        let mut tx = Transaction {
429            actions,
430            nonce,
431            account,
432            signer: signer.public_key(),
433            signature: Default::default(),
434        };
435        tx.sign(signer)?;
436
437        let request_id = self
438            .next_request_id
439            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
440
441        // Build JSON body via tx serialization
442        let body = serde_json::to_string(&tx)?;
443        let json = format!(
444            r#"{{"method":"post","request":{{"type":"action","payload":{}}},"id":{}}}"#,
445            body, request_id
446        );
447
448        let (resp_tx, resp_rx) = oneshot::channel();
449
450        self.cmd_tx
451            .send(Command::Tx {
452                request_id,
453                json,
454                respond: resp_tx,
455            })
456            .await
457            .map_err(|_| eyre::eyre!("client is disconnected — call connect() to reconnect"))?;
458
459        match time::timeout(self.default_timeout, resp_rx).await {
460            Ok(Ok(result)) => result,
461            Ok(Err(_)) => bail!("response channel dropped"),
462            Err(_) => bail!("order request {request_id} timed out"),
463        }
464    }
465
466    /// Send oracle price updates and return the exchange's responses.
467    ///
468    /// Waits for the exchange to acknowledge the transaction so that any
469    /// rejection (e.g. invalid price, authorisation failure) is surfaced to
470    /// the caller rather than silently dropped.
471    ///
472    /// # Arguments
473    /// - `actions`: list of oracle price updates
474    /// - `account`: optional override for the signing account
475    /// - `nonce`: optional nonce override; a fresh one is generated when `None`
476    ///
477    /// # Returns
478    /// - `Ok(responses)` — one [`Response`] per submitted price; callers should
479    ///   inspect each entry with [`Response::is_error`] to detect rejections.
480    /// - `Err(_)` — transport-level failure (send error, timeout, dropped channel).
481    pub async fn update_oracle(
482        &self,
483        actions: Vec<Price>,
484        account: Option<Pubkey>,
485        nonce: Option<u64>,
486    ) -> eyre::Result<Vec<Response>> {
487        let signer = self
488            .signer
489            .as_ref()
490            .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
491
492        let account = if let Some(account) = account {
493            account
494        } else {
495            signer.public_key()
496        };
497
498        let nonce = nonce.unwrap_or_else(make_nonce);
499
500        // Build + sign the transaction
501        let mut tx = Transaction {
502            actions: actions.iter().map(|a| a.clone().into()).collect(),
503            nonce,
504            account,
505            signer: signer.public_key(),
506            signature: Default::default(),
507        };
508        tx.sign(signer)?;
509
510        let request_id = self
511            .next_request_id
512            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
513
514        // Build JSON body via tx serialization
515        let body = serde_json::to_string(&tx)?;
516        let json = format!(
517            r#"{{"method":"post","request":{{"type":"action","payload":{}}},"id":{}}}"#,
518            body, request_id
519        );
520
521        let (resp_tx, resp_rx) = oneshot::channel();
522
523        self.cmd_tx
524            .send(Command::Tx {
525                request_id,
526                json,
527                respond: resp_tx,
528            })
529            .await
530            .map_err(|_| eyre::eyre!("client is disconnected — call connect() to reconnect"))?;
531
532        match time::timeout(self.default_timeout, resp_rx).await {
533            Ok(Ok(result)) => result,
534            Ok(Err(_)) => bail!("oracle update response channel dropped"),
535            Err(_) => bail!("oracle update request {request_id} timed out"),
536        }
537    }
538
539    // ── Convenience wrappers ─────────────────────────────────────────────
540
541    /// Place limit order
542    ///
543    /// # Arguments
544    /// - `symbol`: which market to execute in
545    /// - `side`: buy or sell
546    /// - `price`: limit price
547    /// - `size`: order size
548    /// - `tif`: time in force
549    /// - `reduce_only`: true if order is reduce only
550    ///
551    /// # Returns
552    /// - response for order placement
553    pub async fn place_limit_order(
554        &self,
555        symbol: &str,
556        side: Side,
557        price: f64,
558        size: f64,
559        tif: TimeInForce,
560        reduce_only: bool,
561        account: Option<Pubkey>,
562        nonce: Option<u64>,
563    ) -> eyre::Result<Response> {
564        let signer = self
565            .signer
566            .as_ref()
567            .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
568
569        let account = if let Some(account) = account {
570            account
571        } else {
572            signer.public_key()
573        };
574
575        let nonce = nonce.unwrap_or_else(make_nonce);
576        let order = LimitOrder {
577            symbol: Arc::from(symbol),
578            is_buy: side == Side::Buy,
579            price,
580            size,
581            tif,
582            reduce_only,
583            iso: false,
584            builder_code: None,
585            meta: ActionMeta {
586                account,
587                nonce,
588                seqno: 0,
589                hash: None,
590            },
591        };
592        let resps = self.place_orders(vec![order.into()], None, None).await?;
593        resps
594            .into_iter()
595            .next()
596            .ok_or_else(|| eyre::eyre!("empty response"))
597    }
598
599    /// Place market order
600    ///
601    /// # Arguments
602    /// - `symbol`: which market to execute in
603    /// - `side`: buy or sell
604    /// - `size`: order size
605    /// - `reduce_only`: true if order is reduce only
606    ///
607    /// # Returns
608    /// - response for order placement
609    pub async fn place_market_order(
610        &self,
611        symbol: &str,
612        side: Side,
613        size: f64,
614        reduce_only: bool,
615        account: Option<Pubkey>,
616        nonce: Option<u64>,
617    ) -> eyre::Result<Response> {
618        let signer = self
619            .signer
620            .as_ref()
621            .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
622
623        let account = if let Some(account) = account {
624            account
625        } else {
626            signer.public_key()
627        };
628
629        let nonce = nonce.unwrap_or_else(make_nonce);
630        let order = MarketOrder {
631            symbol: Arc::from(symbol),
632            is_buy: side == Side::Buy,
633            size,
634            reduce_only,
635            iso: false,
636            builder_code: None,
637            meta: ActionMeta {
638                account,
639                nonce,
640                seqno: 0,
641                hash: None,
642            },
643        };
644
645        let resps = self.place_orders(vec![order.into()], None, None).await?;
646        resps
647            .into_iter()
648            .next()
649            .ok_or_else(|| eyre::eyre!("empty response"))
650    }
651
652    /// Cancel order
653    ///
654    /// # Arguments
655    /// - `symbol`: which market to execute in
656    /// - `order_id`: order ID to cancel
657    ///
658    /// # Returns
659    /// - response for order cancel
660    pub async fn cancel_order(
661        &self,
662        symbol: &str,
663        order_id: &str,
664        account: Option<Pubkey>,
665        nonce: Option<u64>,
666    ) -> eyre::Result<Response> {
667        let signer = self
668            .signer
669            .as_ref()
670            .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
671
672        let account = if let Some(account) = account {
673            account
674        } else {
675            signer.public_key()
676        };
677
678        let nonce = nonce.unwrap_or_else(make_nonce);
679        let cancel = CancelOrder {
680            symbol: symbol.to_string(),
681            oid: Hash::from_str(&order_id)?,
682            meta: ActionMeta {
683                account,
684                nonce,
685                seqno: 0,
686                hash: None,
687            },
688        };
689
690        let resps = self.place_orders(vec![cancel.into()], None, None).await?;
691        resps
692            .into_iter()
693            .next()
694            .ok_or_else(|| eyre::eyre!("empty response"))
695    }
696
697    /// Cancel all order
698    ///
699    /// # Arguments
700    /// - `symbols`: which symbols to cancel
701    ///
702    /// # Returns
703    /// - response for order cancel
704    pub async fn cancel_all(
705        &self,
706        symbols: Vec<String>,
707        account: Option<Pubkey>,
708        nonce: Option<u64>,
709    ) -> eyre::Result<Response> {
710        let signer = self
711            .signer
712            .as_ref()
713            .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
714
715        let account = if let Some(account) = account {
716            account
717        } else {
718            signer.public_key()
719        };
720
721        let nonce = nonce.unwrap_or_else(make_nonce);
722        let cancel = CancelAll {
723            symbols,
724            meta: ActionMeta {
725                account,
726                nonce,
727                seqno: 0,
728                hash: None,
729            },
730        };
731        let resps = self.place_orders(vec![cancel.into()], None, None).await?;
732        resps
733            .into_iter()
734            .next()
735            .ok_or_else(|| eyre::eyre!("empty response"))
736    }
737
738    /// Approve a builder-code recipient for routed orders.
739    ///
740    /// Builder codes are encoded as builder-code fees on the wire.
741    pub async fn approve_builder_code(
742        &self,
743        to: Pubkey,
744        fee: u8,
745        account: Option<Pubkey>,
746        nonce: Option<u64>,
747    ) -> eyre::Result<Response> {
748        let signer = self
749            .signer
750            .as_ref()
751            .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
752
753        let account = if let Some(account) = account {
754            account
755        } else {
756            signer.public_key()
757        };
758
759        let nonce = nonce.unwrap_or_else(make_nonce);
760        let action = ApproveCommissionFee {
761            to,
762            max_fee: fee,
763            meta: ActionMeta {
764                account,
765                nonce,
766                seqno: 0,
767                hash: None,
768            },
769        };
770
771        let resps = self.place_orders(vec![action.into()], None, None).await?;
772        resps
773            .into_iter()
774            .next()
775            .ok_or_else(|| eyre::eyre!("empty response"))
776    }
777
778    /// Revoke a builder-code recipient approval.
779    pub async fn revoke_builder_code(
780        &self,
781        to: Pubkey,
782        account: Option<Pubkey>,
783        nonce: Option<u64>,
784    ) -> eyre::Result<Response> {
785        let signer = self
786            .signer
787            .as_ref()
788            .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
789
790        let account = if let Some(account) = account {
791            account
792        } else {
793            signer.public_key()
794        };
795
796        let nonce = nonce.unwrap_or_else(make_nonce);
797        let action = RevokeCommissionFee {
798            to,
799            meta: ActionMeta {
800                account,
801                nonce,
802                seqno: 0,
803                hash: None,
804            },
805        };
806
807        let resps = self.place_orders(vec![action.into()], None, None).await?;
808        resps
809            .into_iter()
810            .next()
811            .ok_or_else(|| eyre::eyre!("empty response"))
812    }
813
814    // ─────────────────────────────────────────────────────────────────────
815    // Subscriptions
816    // ─────────────────────────────────────────────────────────────────────
817
818    /// Subscribe to disconnect notifications.
819    ///
820    /// The returned receiver fires exactly once, carrying the human-readable
821    /// disconnect reason, when the actor exits for any reason (server close,
822    /// network error, or explicit [`shutdown`]).
823    ///
824    /// Use this as a *poison pill* for any tasks you spawned that should stop
825    /// when the connection is lost:
826    ///
827    /// ```text
828    /// let mut rx = client.subscribe_disconnect();
829    /// tokio::spawn(async move {
830    ///     let _ = rx.recv().await; // blocks until disconnect
831    ///     // clean up your task here
832    /// });
833    /// ```
834    pub fn subscribe_disconnect(&self) -> broadcast::Receiver<String> {
835        self.disconnect_tx.subscribe()
836    }
837
838    /// Subscribe to ticker for `symbol`.
839    ///
840    /// # Arguments
841    /// - `symbol`: symbol to subscrive to
842    pub async fn subscribe_ticker(&self, symbol: &str) -> eyre::Result<()> {
843        self.subscribe(vec![SubscriptionRequest::new(
844            "ticker",
845            json!({ "symbol": symbol }),
846        )])
847        .await
848    }
849
850    /// Subscribe to fills for `symbol`.
851    ///
852    /// # Arguments
853    /// - `symbols`: list of symbol to subscribe to
854    pub async fn subscribe_trades(&self, symbols: &[&str]) -> eyre::Result<()> {
855        let subs = symbols
856            .iter()
857            .map(|s| SubscriptionRequest::new("trades", json!({ "symbol": s })))
858            .collect();
859        self.subscribe(subs).await
860    }
861
862    /// Subscribe to L2 snapshots for `symbol`.
863    ///
864    /// # Arguments
865    /// - `symbol`: symbol to subscribe to
866    pub async fn subscribe_l2_snapshot(
867        &self,
868        symbol: &str,
869        nlevels: Option<u32>,
870    ) -> eyre::Result<()> {
871        let mut params = json!({ "symbol": symbol });
872        if let Some(n) = nlevels {
873            params["nlevels"] = json!(n);
874        }
875        self.subscribe(vec![SubscriptionRequest::new("l2Snapshot", params)])
876            .await
877    }
878
879    /// Subscribe to L2 deltas for `symbol`.
880    ///
881    /// # Arguments
882    /// - `symbol`: symbol to subscribe to
883    pub async fn subscribe_l2_delta(&self, symbol: &str) -> eyre::Result<()> {
884        self.subscribe(vec![SubscriptionRequest::new(
885            "l2Delta",
886            json!({ "symbol": symbol }),
887        )])
888        .await
889    }
890
891    /// Subscribe to candles for `symbol`.
892    ///
893    /// # Arguments
894    /// - `symbol`: symbol to subscribe to
895    /// - `interval`: bar period ("1min", "5min", ...)
896    pub async fn subscribe_candles(&self, symbol: &str, interval: &str) -> eyre::Result<()> {
897        self.subscribe(vec![SubscriptionRequest::new(
898            "candle",
899            json!({ "symbol": symbol, "interval": interval }),
900        )])
901        .await
902    }
903
904    /// Subscribe list of subscription requests
905    ///
906    /// # Arguments
907    /// - `subs`: subscription list
908    async fn subscribe(&self, subs: Vec<SubscriptionRequest>) -> eyre::Result<()> {
909        self.cmd_tx
910            .send(Command::Subscribe(subs))
911            .await
912            .map_err(|_| eyre::eyre!("actor gone"))?;
913        Ok(())
914    }
915
916    // ─────────────────────────────────────────────────────────────────────
917    // Event handlers
918    // ─────────────────────────────────────────────────────────────────────
919
920    /// Register a callback for a topic. The callback runs synchronously
921    /// inside the actor loop — keep it fast or `tokio::spawn` from within.
922    ///
923    /// # Argument
924    /// - `topic`: topic to subscribe to
925    /// - `handler`: callback for topic
926    pub async fn on(&self, topic: Topic, handler: impl Fn(&Event) + Send + Sync + 'static) {
927        self.handlers
928            .lock()
929            .unwrap()
930            .entry(topic)
931            .or_default()
932            .push(Box::new(handler));
933    }
934}
935
936// ═════════════════════════════════════════════════════════════════════════════
937// Actor — owns all mutable state, runs in a single task
938// ═════════════════════════════════════════════════════════════════════════════
939
940type WsWriter = SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>;
941type WsReader = futures_util::stream::SplitStream<WebSocketStream<MaybeTlsStream<TcpStream>>>;
942
943struct Actor {
944    // WebSocket write half
945    ws_write: WsWriter,
946    // Event sender
947    event_tx: mpsc::Sender<(Topic, Event)>,
948
949    // Inbound commands from the client handle
950    cmd_rx: mpsc::Receiver<Command>,
951
952    // ── Watch senders (actor pushes, clients read) ─────────────────────
953    ticker_tx: watch::Sender<HashMap<String, Ticker>>,
954    account_tx: watch::Sender<AccountState>,
955
956    // ── Owned state (no locks) ─────────────────────────────────────────
957    tickers: HashMap<String, Ticker>,
958    prices: HashMap<String, f64>,
959    account_state: AccountState,
960    pending: HashMap<u64, oneshot::Sender<eyre::Result<Vec<Response>>>>,
961
962    // Subscription log (for reconnection replay)
963    subscriptions: Vec<SubscriptionRequest>,
964
965    // ── Disconnect signalling ─────────────────────────────────────────────
966    /// Shared flag — flipped to `false` before the `Disconnected` event fires.
967    connected: Arc<AtomicBool>,
968    /// Broadcast once with the disconnect reason when the actor exits.
969    disconnect_tx: broadcast::Sender<String>,
970}
971
972impl Actor {
973    /// Run loop
974    async fn run(mut self, mut ws_read: WsReader, initial_subs: Vec<SubscriptionRequest>) {
975        // Send initial subscriptions
976        if !initial_subs.is_empty() {
977            if let Err(e) = self.send_subscribe(&initial_subs).await {
978                error!("Initial subscription failed: {e}");
979                return;
980            }
981            self.subscriptions = initial_subs;
982        }
983
984        // Emit Connected so handlers can react immediately.
985        self.emit(Topic::Status, &Event::Connected);
986
987        // Use a labelled loop so every exit path carries an explicit reason.
988        let disconnect_reason: String = 'actor: loop {
989            tokio::select! {
990                // Inbound WebSocket message
991                msg = ws_read.next() => {
992                    match msg {
993                        Some(Ok(Message::Text(text))) => {
994                            debug!("msg {}: {}", text.len(), &text[0..512.min(text.len())]);
995                            match serde_json::from_str::<Value>(&text) {
996                                Ok(data) => self.handle_message(data, &text).await,
997                                Err(e) => error!("JSON decode error: {e}"),
998                            }
999                        }
1000                        Some(Ok(Message::Close(_))) => {
1001                            warn!("WebSocket closed by server");
1002                            break 'actor "server closed the connection".into();
1003                        }
1004                        Some(Err(e)) => {
1005                            error!("WebSocket read error: {e}");
1006                            break 'actor format!("WebSocket read error: {e}");
1007                        }
1008                        None => {
1009                            warn!("WebSocket stream ended");
1010                            break 'actor "WebSocket stream ended".into();
1011                        }
1012                        _ => {} // Ping/Pong handled by tungstenite
1013                    }
1014                }
1015
1016                // Command from client handle
1017                cmd = self.cmd_rx.recv() => {
1018                    match cmd {
1019                        Some(Command::Subscribe(subs)) => {
1020                            if let Err(e) = self.send_subscribe(&subs).await {
1021                                error!("Subscription send error: {e}");
1022                            }
1023                            self.subscriptions.extend(subs);
1024                        }
1025
1026                        Some(Command::Tx { request_id, json, respond }) => {
1027                            self.pending.insert(request_id, respond);
1028                            if let Err(e) = self.ws_send_text(&json).await {
1029                                error!("Order send error: {e}");
1030                                if let Some(tx) = self.pending.remove(&request_id) {
1031                                    let _ = tx.send(Err(e));
1032                                }
1033                            }
1034                        }
1035
1036                        Some(Command::AsyncTx { json}) => {
1037                            if let Err(e) = self.ws_send_text(&json).await {
1038                                error!("Order send error: {e}");
1039                            }
1040                        }
1041
1042                        Some(Command::SendRaw(json)) => {
1043                            if let Err(e) = self.ws_send_text(&json).await {
1044                                error!("Raw send error: {e}");
1045                            }
1046                        }
1047
1048                        Some(Command::GetOrders { symbol, respond }) => {
1049                            let orders = match symbol {
1050                                Some(s) => self.account_state.open_orders
1051                                    .values()
1052                                    .filter(|o| o.symbol == s)
1053                                    .cloned()
1054                                    .collect(),
1055                                None => self.account_state.open_orders.values().cloned().collect(),
1056                            };
1057                            let _ = respond.send(orders);
1058                        }
1059
1060                        Some(Command::Shutdown) | None => {
1061                            info!("Actor shutting down (requested)");
1062                            break 'actor "shutdown requested".into();
1063                        }
1064                    }
1065                }
1066            }
1067        }; // end 'actor loop
1068
1069        self.handle_disconnect(disconnect_reason).await;
1070    }
1071
1072    /// Shared teardown called from every exit path in `run()`.
1073    ///
1074    /// Order of operations:
1075    /// 1. Flip `connected` flag so callers see `is_connected() == false` immediately.
1076    /// 2. Fail every in-progress `place_orders` that is waiting on a oneshot response.
1077    /// 3. Emit `Event::Disconnected` on `Topic::Status` so registered handlers fire.
1078    /// 4. Broadcast the reason string to all `subscribe_disconnect()` receivers
1079    ///    — this is the poison pill for any other spawned tasks.
1080    /// 5. Close the WebSocket write half.
1081    async fn handle_disconnect(&mut self, reason: String) {
1082        // 1. Mark disconnected — visible to all handles immediately.
1083        self.connected.store(false, Ordering::Release);
1084
1085        // 2. Fail every pending order response.
1086        let err_msg = format!("disconnected: {reason}");
1087        for (_, tx) in self.pending.drain() {
1088            let _ = tx.send(Err(eyre::eyre!("{}", err_msg)));
1089        }
1090
1091        // 3. Emit the Disconnected event to registered handlers.
1092        self.emit(Topic::Status, &Event::Disconnected(reason.clone()));
1093
1094        // 4. Broadcast reason as poison pill (best-effort; ignore no-receivers).
1095        let _ = self.disconnect_tx.send(reason.clone());
1096
1097        // 5. Close WS write half (ignore error — connection may already be gone).
1098        let _ = self.ws_write.close().await;
1099
1100        info!("Actor stopped: {reason}");
1101    }
1102
1103    /// WS send
1104    async fn ws_send_text(&mut self, text: &str) -> eyre::Result<()> {
1105        let len = text.len();
1106        debug!("sending msg len: {}", len);
1107        self.ws_write
1108            .send(Message::Text(text.into()))
1109            .await
1110            .map_err(|e| eyre::eyre!("ws write: {e}"))?;
1111        Ok(())
1112    }
1113
1114    // ─────────────────────────────────────────────────────────────────────
1115    // Message dispatch
1116    // ─────────────────────────────────────────────────────────────────────
1117
1118    async fn handle_message(&mut self, data: Value, json: &str) {
1119        let msg_type = data["type"].as_str().unwrap_or("");
1120
1121        match msg_type {
1122            "subscriptionResponse" => {
1123                info!(
1124                    "Subscription confirmed: {:?}",
1125                    data["topics"].as_array().map(|a| a.len())
1126                );
1127            }
1128
1129            "ticker" => {
1130                let ticker_v = &data["data"]["ticker"];
1131                if let Ok(ticker) = serde_json::from_value::<Ticker>(ticker_v.clone()) {
1132                    self.prices.insert(ticker.symbol.clone(), ticker.mark_price);
1133                    self.tickers.insert(ticker.symbol.clone(), ticker.clone());
1134
1135                    // Push watch updates
1136                    let _ = self.ticker_tx.send(self.tickers.clone());
1137
1138                    self.emit(Topic::Ticker, &Event::Ticker(ticker.clone()));
1139                    debug!("Ticker: {} mark={:.2}", ticker.symbol, ticker.mark_price);
1140                } else {
1141                    error!("Could not parse ticker event: {:?}", ticker_v);
1142                }
1143            }
1144
1145            "trades" => {
1146                if let Ok(trades) = serde_json::from_value::<Vec<Fill>>(data["data"].clone()) {
1147                    self.emit(Topic::Trades, &Event::Trades(trades));
1148                } else {
1149                    error!("Could not parse trades event: {:?}", data["data"]);
1150                }
1151            }
1152
1153            "l2Snapshot" => {
1154                if let Ok(l2_snapshot) =
1155                    serde_json::from_value::<L2Snapshot>(data["data"]["book"].clone())
1156                {
1157                    self.emit(Topic::L2Snapshot, &Event::L2Snapshot(l2_snapshot));
1158                } else {
1159                    error!("Could not parse l2_snapshot event: msg: {:?}", data["data"]);
1160                }
1161            }
1162
1163            "l2Delta" => {
1164                if let Ok(l2_delta) =
1165                    serde_json::from_value::<L2Snapshot>(data["data"]["book"].clone())
1166                {
1167                    self.emit(Topic::L2Delta, &Event::L2Delta(l2_delta));
1168                } else {
1169                    error!("Could not parse l2_delta event: {:?}", data["data"]);
1170                }
1171            }
1172
1173            "candle" => {
1174                if let Ok(candle) = serde_json::from_value::<Candle>(data["data"].clone()) {
1175                    self.emit(Topic::Candle, &Event::Candle(candle));
1176                } else {
1177                    error!("Could not parse candle event: {:?}", data["data"]);
1178                }
1179            }
1180
1181            "account" => {
1182                self.handle_account(&data["data"]).await;
1183            }
1184
1185            "post" => {
1186                self.handle_post_response(&data, json);
1187            }
1188
1189            other => {
1190                debug!("Unhandled message type: {other}");
1191            }
1192        }
1193    }
1194
1195    // ─────────────────────────────────────────────────────────────────────
1196    // Account updates
1197    // ─────────────────────────────────────────────────────────────────────
1198
1199    async fn handle_account(&mut self, data: &Value) {
1200        let update_type = data["type"].as_str().unwrap_or("");
1201
1202        match update_type {
1203            "accountSnapshot" => {
1204                if let Ok(margin) = serde_json::from_value::<Margin>(data["margin"].clone()) {
1205                    self.account_state.margin = margin.clone();
1206                    self.emit(Topic::Margin, &Event::Margin(margin))
1207                }
1208
1209                if let Ok(positions) =
1210                    serde_json::from_value::<Vec<PositionInfo>>(data["positions"].clone())
1211                {
1212                    for position in &positions {
1213                        self.emit(Topic::Position, &Event::Position(position.clone()))
1214                    }
1215                    self.account_state.positions = positions
1216                        .into_iter()
1217                        .map(|p| (p.symbol.clone(), p.clone()))
1218                        .collect();
1219                }
1220
1221                if let Ok(orders) =
1222                    serde_json::from_value::<Vec<OrderState>>(data["openOrders"].clone())
1223                {
1224                    for order in &orders {
1225                        self.emit(Topic::Order, &Event::Order(order.clone()))
1226                    }
1227                    self.account_state.open_orders = orders
1228                        .into_iter()
1229                        .map(|o| (o.order_id.clone(), o))
1230                        .collect();
1231                }
1232
1233                if let Ok(leverages) =
1234                    serde_json::from_value::<Vec<LeverageSetting>>(data["leverageSettings"].clone())
1235                {
1236                    self.emit(Topic::Leverage, &Event::Leverage(leverages.clone()));
1237                    for l in leverages {
1238                        self.account_state
1239                            .leverage_settings
1240                            .insert(l.symbol.clone(), l);
1241                    }
1242                }
1243
1244                if let Ok(approvals) = serde_json::from_value::<Vec<CommissionApproval>>(
1245                    data["builderCodeApprovals"].clone(),
1246                ) {
1247                    self.account_state.commission_approvals = approvals;
1248                }
1249
1250                info!(
1251                    "Account snapshot: balance={:.2}, positions={}, orders={}",
1252                    self.account_state.margin.total_balance,
1253                    self.account_state.positions.len(),
1254                    self.account_state.open_orders.len(),
1255                );
1256            }
1257
1258            "orderUpdate" => {
1259                if let Ok(order) = serde_json::from_value::<OrderState>(data.clone()) {
1260                    let oid = order.order_id.clone();
1261                    if order.status.is_terminal() {
1262                        self.account_state.open_orders.remove(&oid);
1263                    } else {
1264                        self.account_state.open_orders.insert(oid, order.clone());
1265                    }
1266                    self.emit(Topic::Order, &Event::Order(order));
1267                } else {
1268                    error!("Could not parse order event: {:?}", data);
1269                }
1270            }
1271
1272            "marginUpdate" => {
1273                if let Ok(margin) = serde_json::from_value::<Margin>(data.clone()) {
1274                    self.account_state.margin = margin.clone();
1275                    self.publish_account();
1276                    self.emit(Topic::Margin, &Event::Margin(margin));
1277                } else {
1278                    error!("Could not parse margin event: {:?}", data);
1279                }
1280            }
1281
1282            "positionUpdate" => {
1283                if let Ok(pos) = serde_json::from_value::<PositionInfo>(data.clone()) {
1284                    self.account_state
1285                        .positions
1286                        .insert(pos.symbol.clone(), pos.clone());
1287                    self.emit(Topic::Position, &Event::Position(pos));
1288                    self.publish_account();
1289                } else {
1290                    error!("Could not parse position event: {:?}", data);
1291                }
1292            }
1293
1294            "fill" => {
1295                if let Ok(fill) = serde_json::from_value::<Fill>(data.clone()) {
1296                    let dir = fill.side.dir();
1297                    if let Some(order) = self.account_state.open_orders.get_mut(&fill.order_id) {
1298                        order.filled_size += fill.size;
1299                        order.signed_size -= dir * fill.size;
1300                        if order.signed_size * dir <= 0.0 {
1301                            self.account_state.open_orders.remove(&fill.order_id);
1302                        }
1303                    }
1304                    self.publish_account();
1305                    self.emit(Topic::Fill, &Event::Fill(fill.clone()));
1306                    info!(
1307                        "Fill: {} {:?} {} @ {} maker={}",
1308                        fill.symbol, fill.side, fill.size, fill.price, fill.is_maker,
1309                    );
1310                } else {
1311                    error!("Could not parse fill event: {:?}", data);
1312                }
1313            }
1314
1315            "leverageUpdate" => {
1316                if let Ok(leverages) =
1317                    serde_json::from_value::<Vec<LeverageSetting>>(data["leverage"].clone())
1318                {
1319                    for lev in &leverages {
1320                        self.account_state
1321                            .leverage_settings
1322                            .insert(lev.symbol.clone(), lev.clone());
1323                    }
1324                    self.emit(Topic::Leverage, &Event::Leverage(leverages.clone()));
1325                    self.publish_account();
1326                } else {
1327                    error!("Could not parse leverage event: {:?}", data);
1328                }
1329            }
1330
1331            _ => {
1332                debug!("Unknown account update: {update_type}");
1333            }
1334        }
1335    }
1336
1337    // ─────────────────────────────────────────────────────────────────────
1338    // Post (order) response
1339    // ─────────────────────────────────────────────────────────────────────
1340
1341    fn handle_post_response(&mut self, data: &Value, _json: &str) {
1342        let request_id = data["id"].as_u64().unwrap_or(0);
1343        let inner = &data["data"];
1344        let rtype = inner["type"].as_str().unwrap_or("");
1345        let sender = self.pending.remove(&request_id);
1346
1347        match rtype {
1348            "action" => {
1349                let payload = &inner["payload"];
1350                let status = payload["status"].as_str().unwrap_or("");
1351
1352                if status != "ok" {
1353                    error!("Order request {request_id} failed: {status}");
1354                    if let Some(tx) = sender {
1355                        let _ = tx.send(Err(eyre::eyre!("order request failed: {}", data)));
1356                    }
1357                    self.emit(Topic::Error, &Event::Error(data.clone()));
1358                } else {
1359                    let responses = Response::parse_responses(data);
1360                    if let Some(tx) = sender {
1361                        let _ = tx.send(Ok(responses));
1362                    }
1363                }
1364            }
1365            "ack" => {
1366                let ok = inner["ok"].as_bool().unwrap_or(false);
1367                let response = if ok {
1368                    Response {
1369                        order_id: None,
1370                        status: "OK".to_string(),
1371                        message: None,
1372                        raw: inner.clone(),
1373                    }
1374                } else {
1375                    let message = inner["message"].as_str().unwrap_or("");
1376                    Response {
1377                        order_id: None,
1378                        status: "Error".to_string(),
1379                        message: Some(message.to_string()),
1380                        raw: inner.clone(),
1381                    }
1382                };
1383                if let Some(tx) = sender {
1384                    let _ = tx.send(Ok(vec![response]));
1385                }
1386            }
1387            _ => panic!("unknown response type: {}", rtype),
1388        }
1389    }
1390
1391    // ─────────────────────────────────────────────────────────────────────
1392    // Helpers
1393    // ─────────────────────────────────────────────────────────────────────
1394
1395    /// Publish the current account state snapshot to the watch channel.
1396    fn publish_account(&self) {
1397        let _ = self.account_tx.send(self.account_state.clone());
1398    }
1399
1400    /// Fire all handlers registered for `topic`.
1401    fn emit(&self, topic: Topic, data: &Event) {
1402        let _ = self.event_tx.try_send((topic, data.clone()));
1403    }
1404
1405    /// Send a JSON value over the WebSocket.
1406    async fn ws_send_json(&mut self, value: &Value) -> eyre::Result<()> {
1407        let text = serde_json::to_string(value)?;
1408        self.ws_write
1409            .send(Message::Text(text.into()))
1410            .await
1411            .map_err(|e| eyre::eyre!("ws write: {e}"))?;
1412        Ok(())
1413    }
1414
1415    /// Send subscription request(s) over the WebSocket.
1416    async fn send_subscribe(&mut self, subs: &[SubscriptionRequest]) -> eyre::Result<()> {
1417        let request = json!({
1418            "method": "subscribe",
1419            "subscription": subs.iter().map(|s| s.to_json()).collect::<Vec<_>>(),
1420        });
1421        self.ws_send_json(&request).await?;
1422        info!("Subscribed to {} topics", subs.len());
1423        Ok(())
1424    }
1425}