Skip to main content

acta_maker_sdk/ws/
client.rs

1use futures_util::stream::{SplitSink, SplitStream};
2use futures_util::{SinkExt, StreamExt};
3use std::sync::{Arc, OnceLock};
4use std::time::Duration;
5use tokio::net::TcpStream;
6
7use tokio_tungstenite::{
8    MaybeTlsStream, WebSocketStream, connect_async_with_config,
9    tungstenite::{
10        Bytes, Message, Utf8Bytes,
11        protocol::{CloseFrame, WebSocketConfig},
12    },
13};
14
15use crate::ws::error::{UnreadableFrame, WsClientError, WsResult, WsTransportConfigError};
16use crate::ws::types::{
17    AcceptQuoteMessage, AddChannelsData, AddMintsData, AuthChallengeData, BatchQuotesMessage,
18    CancelAllQuotesMessage, CancelQuoteData, CancelRfqData, ClaimReferralCodeData, ClientMessage,
19    CloseCode, GetActiveRfqsMessage, GetExpiriesMessage, GetIndicativePricesMessage,
20    GetMakerPositionsMessage, GetMarketDescriptorsMessage, GetMarketsForMakerMessage,
21    GetMarketsMessage, GetMmSummaryMessage, GetMyActiveRfqsMessage, GetMyQuotesMessage,
22    GetMyReferralInfoData, GetOrderStatusMessage, GetPositionsMessage, GetSubscriptionsMessage,
23    GetTokensMessage, HelloData, IndicativePricesResponseMessage, Lane, QuoteMessage,
24    RedeemInviteData, RemoveChannelsData, RemoveMintsData, ReplaceQuoteMessage, ResumeAuthData,
25    RfqRequestMessage, ServerMessage, StartAuthData, SubmitSignedSponsoredTxData, SubscribeData,
26    UnsubscribeData, WsChannel, parse_server_message,
27};
28use uuid::Uuid;
29
30type Socket = WebSocketStream<MaybeTlsStream<TcpStream>>;
31pub(crate) type WsSink = SplitSink<Socket, Message>;
32
33macro_rules! ws_method {
34    ($name:ident, $variant:ident, $data:ty) => {
35        pub async fn $name(&mut self, data: $data) -> WsResult<()> {
36            self.send(&ClientMessage::$variant(data)).await
37        }
38    };
39}
40
41macro_rules! ws_method_request_id {
42    ($name:ident, $variant:ident, $msg:ident) => {
43        pub async fn $name(&mut self) -> WsResult<()> {
44            self.send(&ClientMessage::$variant($msg {
45                request_id: Uuid::new_v4(),
46                limit: 200,
47                cursor: crate::ws::types::Cursor::Start {},
48            }))
49            .await
50        }
51    };
52}
53
54/// Low-level WebSocket transport.
55///
56/// [`Self::send_hello`] deliberately retains no handshake state. Callers using
57/// this raw client must wait for `Welcome` and validate it with
58/// Check the Welcome protocol version before sending authentication or
59/// exposing the frame to application code. [`crate::ws::managed`] and the
60/// vault fleet enforce that sequence themselves.
61pub struct WsClient {
62    stream: Socket,
63    server_close: Arc<OnceLock<ServerClose>>,
64    stream_label: &'static str,
65}
66
67/// Read half of a split [`WsClient`]. Liveness frames are returned, not
68/// answered, so the owner decides which lane the reply takes.
69pub(crate) struct WsReader {
70    stream: SplitStream<Socket>,
71    server_close: Arc<OnceLock<ServerClose>>,
72    stream_label: &'static str,
73}
74
75/// The close frame the server sent.
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct ServerClose {
78    pub code: u16,
79    pub reason: String,
80}
81
82impl ServerClose {
83    /// `None` for codes outside [`CloseCode`].
84    #[must_use]
85    pub const fn close_code(&self) -> Option<CloseCode> {
86        CloseCode::from_code(self.code)
87    }
88}
89
90fn record_close(slot: &OnceLock<ServerClose>, frame: Option<CloseFrame>) {
91    if let Some(frame) = frame {
92        let _ = slot.set(ServerClose {
93            code: u16::from(frame.code),
94            reason: frame.reason.to_string(),
95        });
96    }
97}
98
99#[allow(clippy::large_enum_variant)]
100pub(crate) enum WsFrame {
101    Message(ServerMessage),
102    Ping(Bytes),
103    Pong,
104}
105
106#[derive(Debug, Clone)]
107pub struct PreparedClientMessage {
108    text: Utf8Bytes,
109    lane: Lane,
110    subscription: Option<Box<crate::ws::types::SubscriptionChange>>,
111}
112
113impl PreparedClientMessage {
114    pub fn new(message: &ClientMessage) -> Result<Self, serde_json::Error> {
115        Ok(Self {
116            text: serde_json::to_string(message)?.into(),
117            lane: message.lane(),
118            subscription: crate::ws::types::SubscriptionChange::from_message(message).map(Box::new),
119        })
120    }
121
122    pub(crate) fn take_subscription(
123        &mut self,
124    ) -> Option<Box<crate::ws::types::SubscriptionChange>> {
125        self.subscription.take()
126    }
127
128    pub(crate) const fn lane(&self) -> Lane {
129        self.lane
130    }
131
132    pub(crate) fn into_text(self) -> Utf8Bytes {
133        self.text
134    }
135
136    pub fn as_str(&self) -> &str {
137        &self.text
138    }
139
140    #[must_use]
141    pub fn len(&self) -> usize {
142        self.text.len()
143    }
144
145    #[must_use]
146    pub fn is_empty(&self) -> bool {
147        self.text.is_empty()
148    }
149}
150
151#[derive(Debug, Clone, Copy)]
152pub struct WsTransportConfig {
153    pub connect_timeout: Duration,
154    pub read_buffer_size: usize,
155    pub write_buffer_size: usize,
156    pub max_write_buffer_size: usize,
157    pub max_message_size: usize,
158    pub max_frame_size: usize,
159    pub tcp_nodelay: bool,
160}
161
162impl Default for WsTransportConfig {
163    fn default() -> Self {
164        Self {
165            connect_timeout: Duration::from_secs(10),
166            read_buffer_size: 64 * 1024,
167            write_buffer_size: 0,
168            max_write_buffer_size: 1024 * 1024,
169            max_message_size: 2 * 1024 * 1024,
170            max_frame_size: 2 * 1024 * 1024,
171            tcp_nodelay: true,
172        }
173    }
174}
175
176impl WsTransportConfig {
177    /// Validate limits before a connection task is spawned.
178    ///
179    /// # Errors
180    /// Returns a typed error for zero deadlines/limits or an invalid write-buffer range.
181    pub fn validate(self) -> Result<(), WsTransportConfigError> {
182        if self.connect_timeout.is_zero() {
183            return Err(WsTransportConfigError::ZeroConnectTimeout);
184        }
185        if self.max_write_buffer_size <= self.write_buffer_size {
186            return Err(WsTransportConfigError::InvalidWriteBufferRange);
187        }
188        if self.max_message_size == 0 || self.max_frame_size == 0 {
189            return Err(WsTransportConfigError::ZeroMessageOrFrameLimit);
190        }
191        Ok(())
192    }
193}
194
195impl WsClient {
196    pub async fn connect(url: &str) -> WsResult<Self> {
197        Self::connect_with_config(url, WsTransportConfig::default()).await
198    }
199
200    pub async fn connect_with_config(url: &str, config: WsTransportConfig) -> WsResult<Self> {
201        config.validate()?;
202        let websocket_config = WebSocketConfig::default()
203            .read_buffer_size(config.read_buffer_size)
204            .write_buffer_size(config.write_buffer_size)
205            .max_write_buffer_size(config.max_write_buffer_size)
206            .max_message_size(Some(config.max_message_size))
207            .max_frame_size(Some(config.max_frame_size));
208        let connect = connect_async_with_config(url, Some(websocket_config), config.tcp_nodelay);
209        let (stream, _response) = tokio::time::timeout(config.connect_timeout, connect)
210            .await
211            .map_err(|_| WsClientError::ConnectTimeout)??;
212        Ok(Self {
213            stream,
214            server_close: Arc::default(),
215            stream_label: if url.ends_with("/data") {
216                "data"
217            } else {
218                "quote"
219            },
220        })
221    }
222
223    pub async fn send(&mut self, msg: &ClientMessage) -> WsResult<()> {
224        let prepared = PreparedClientMessage::new(msg)?;
225        self.send_prepared(&prepared).await?;
226        Ok(())
227    }
228
229    pub async fn send_prepared(&mut self, message: &PreparedClientMessage) -> WsResult<()> {
230        self.stream
231            .send(Message::Text(message.text.clone()))
232            .await?;
233        Ok(())
234    }
235
236    /// Send a prepared message by transferring its buffer into the socket sink.
237    pub async fn send_prepared_owned(&mut self, message: PreparedClientMessage) -> WsResult<()> {
238        self.stream.send(Message::Text(message.text)).await?;
239        Ok(())
240    }
241
242    ws_method!(send_hello, Hello, HelloData);
243    ws_method!(resume_auth, ResumeAuth, ResumeAuthData);
244    ws_method!(auth_challenge, AuthChallenge, AuthChallengeData);
245    ws_method!(quote, Quote, QuoteMessage);
246    ws_method!(replace_quote, ReplaceQuote, ReplaceQuoteMessage);
247    ws_method!(batch_quotes, BatchQuotes, BatchQuotesMessage);
248    ws_method!(subscribe, Subscribe, SubscribeData);
249    ws_method!(unsubscribe, Unsubscribe, UnsubscribeData);
250    ws_method!(get_my_quotes, GetMyQuotes, GetMyQuotesMessage);
251    ws_method!(
252        get_maker_positions,
253        GetMakerPositions,
254        GetMakerPositionsMessage
255    );
256    ws_method!(
257        get_markets_for_maker,
258        GetMarketsForMaker,
259        GetMarketsForMakerMessage
260    );
261    ws_method!(rfq_request, RfqRequest, RfqRequestMessage);
262    ws_method!(accept_quote, AcceptQuote, AcceptQuoteMessage);
263    ws_method!(
264        submit_signed_sponsored_tx,
265        SubmitSignedSponsoredTx,
266        SubmitSignedSponsoredTxData
267    );
268    ws_method!(
269        indicative_prices_response,
270        IndicativePricesResponse,
271        IndicativePricesResponseMessage
272    );
273    ws_method!(
274        get_indicative_prices,
275        GetIndicativePrices,
276        GetIndicativePricesMessage
277    );
278    ws_method!(
279        get_market_descriptors,
280        GetMarketDescriptors,
281        GetMarketDescriptorsMessage
282    );
283    ws_method!(get_tokens, GetTokens, GetTokensMessage);
284    ws_method!(get_expiries, GetExpiries, GetExpiriesMessage);
285    ws_method!(get_order_status, GetOrderStatus, GetOrderStatusMessage);
286    ws_method!(redeem_invite, RedeemInvite, RedeemInviteData);
287    ws_method!(
288        claim_referral_code,
289        ClaimReferralCode,
290        ClaimReferralCodeData
291    );
292    ws_method!(
293        get_my_referral_info,
294        GetMyReferralInfo,
295        GetMyReferralInfoData
296    );
297
298    ws_method!(get_mm_summary, GetMmSummary, GetMmSummaryMessage);
299    ws_method!(
300        get_position_summary,
301        GetPositionSummary,
302        crate::ws::types::GetPositionSummaryMessage
303    );
304    ws_method_request_id!(get_subscriptions, GetSubscriptions, GetSubscriptionsMessage);
305    ws_method_request_id!(get_markets, GetMarkets, GetMarketsMessage);
306
307    ws_method!(get_positions, GetPositions, GetPositionsMessage);
308    ws_method!(get_my_active_rfqs, GetMyActiveRfqs, GetMyActiveRfqsMessage);
309    ws_method!(get_active_rfqs, GetActiveRfqs, GetActiveRfqsMessage);
310
311    pub async fn ping(&mut self) -> WsResult<()> {
312        self.send(&ClientMessage::Ping).await
313    }
314
315    pub async fn logout(&mut self) -> WsResult<()> {
316        self.send(&ClientMessage::Logout).await
317    }
318
319    pub async fn start_auth(&mut self, pubkey: crate::Address32) -> WsResult<()> {
320        self.send(&ClientMessage::StartAuth(StartAuthData { pubkey }))
321            .await
322    }
323
324    pub async fn cancel_quote(&mut self, rfq_id: Uuid) -> WsResult<()> {
325        self.send(&ClientMessage::CancelQuote(CancelQuoteData {
326            rfq_id,
327            request_id: Uuid::new_v4(),
328        }))
329        .await
330    }
331
332    pub async fn cancel_all_quotes(
333        &mut self,
334        market: crate::ws::types::QueryFilter<crate::Address32>,
335    ) -> WsResult<()> {
336        self.send(&ClientMessage::CancelAllQuotes(CancelAllQuotesMessage {
337            request_id: Uuid::new_v4(),
338            market,
339        }))
340        .await
341    }
342
343    pub async fn cancel_rfq(&mut self, rfq_id: Uuid) -> WsResult<()> {
344        self.send(&ClientMessage::CancelRfq(CancelRfqData {
345            rfq_id,
346            request_id: Uuid::new_v4(),
347        }))
348        .await
349    }
350
351    pub async fn add_mints(
352        &mut self,
353        underlying_mints: Vec<crate::Address32>,
354        quote_mints: Vec<crate::Address32>,
355    ) -> WsResult<()> {
356        self.send(&ClientMessage::AddMints(AddMintsData {
357            request_id: Uuid::new_v4(),
358            underlying_mints,
359            quote_mints,
360        }))
361        .await
362    }
363
364    pub async fn remove_mints(
365        &mut self,
366        underlying_mints: Vec<crate::Address32>,
367        quote_mints: Vec<crate::Address32>,
368    ) -> WsResult<()> {
369        self.send(&ClientMessage::RemoveMints(RemoveMintsData {
370            request_id: Uuid::new_v4(),
371            underlying_mints,
372            quote_mints,
373        }))
374        .await
375    }
376
377    pub async fn add_channels(&mut self, channels: Vec<WsChannel>) -> WsResult<()> {
378        self.send(&ClientMessage::AddChannels(AddChannelsData {
379            request_id: Uuid::new_v4(),
380            channels,
381        }))
382        .await
383    }
384
385    pub async fn remove_channels(&mut self, channels: Vec<WsChannel>) -> WsResult<()> {
386        self.send(&ClientMessage::RemoveChannels(RemoveChannelsData {
387            request_id: Uuid::new_v4(),
388            channels,
389        }))
390        .await
391    }
392
393    pub async fn send_text(&mut self, text: impl Into<String>) -> WsResult<()> {
394        self.stream.send(Message::Text(text.into().into())).await?;
395        Ok(())
396    }
397
398    /// The server's close frame once [`Self::next`] has returned `None` after one.
399    #[must_use]
400    pub fn server_close(&self) -> Option<&ServerClose> {
401        self.server_close.get()
402    }
403
404    pub(crate) fn server_close_slot(&self) -> Arc<OnceLock<ServerClose>> {
405        Arc::clone(&self.server_close)
406    }
407
408    /// Read until the server's close frame or end of stream, at most `grace`.
409    pub(crate) async fn await_server_close(&mut self, grace: Duration) {
410        let _ = tokio::time::timeout(grace, async {
411            while let Some(Ok(_)) = self.next().await {}
412        })
413        .await;
414    }
415
416    pub async fn next(&mut self) -> Option<Result<ServerMessage, WsClientError>> {
417        loop {
418            let msg = match self.stream.next().await {
419                Some(Ok(msg)) => {
420                    crate::io_observer::record(crate::io_observer::Event::Ws {
421                        venue: "acta",
422                        stream: self.stream_label,
423                        bytes: msg.len(),
424                    });
425                    msg
426                }
427                Some(Err(err)) => return Some(Err(err.into())),
428                None => return None,
429            };
430
431            match msg {
432                Message::Text(text) => {
433                    return Some(parse_frame(text.as_bytes()));
434                }
435                Message::Binary(bin) => {
436                    return Some(parse_frame(&bin));
437                }
438                Message::Ping(payload) => {
439                    if let Err(err) = self.stream.send(Message::Pong(payload)).await {
440                        return Some(Err(err.into()));
441                    }
442                }
443                Message::Pong(_) => {}
444                Message::Close(frame) => {
445                    record_close(&self.server_close, frame);
446                    return None;
447                }
448                Message::Frame(_) => {}
449            }
450        }
451    }
452
453    pub async fn close(mut self) -> WsResult<()> {
454        self.stream.close(None).await?;
455        Ok(())
456    }
457
458    pub(crate) fn split(self) -> (WsSink, WsReader) {
459        let (sink, stream) = self.stream.split();
460        (
461            sink,
462            WsReader {
463                stream,
464                server_close: self.server_close,
465                stream_label: self.stream_label,
466            },
467        )
468    }
469}
470
471impl WsReader {
472    /// Read until the server's close frame or end of stream, at most `grace`.
473    pub(crate) async fn await_server_close(&mut self, grace: Duration) {
474        let _ = tokio::time::timeout(grace, async {
475            while let Some(Ok(_)) = self.next().await {}
476        })
477        .await;
478    }
479
480    pub(crate) async fn next(&mut self) -> Option<WsResult<WsFrame>> {
481        loop {
482            let msg = match self.stream.next().await {
483                Some(Ok(msg)) => {
484                    crate::io_observer::record(crate::io_observer::Event::Ws {
485                        venue: "acta",
486                        stream: self.stream_label,
487                        bytes: msg.len(),
488                    });
489                    msg
490                }
491                Some(Err(err)) => return Some(Err(err.into())),
492                None => return None,
493            };
494            let frame = match msg {
495                Message::Text(text) => parse_frame(text.as_bytes()).map(WsFrame::Message),
496                Message::Binary(bin) => parse_frame(&bin).map(WsFrame::Message),
497                Message::Ping(payload) => Ok(WsFrame::Ping(payload)),
498                Message::Pong(_) => Ok(WsFrame::Pong),
499                Message::Close(frame) => {
500                    record_close(&self.server_close, frame);
501                    return None;
502                }
503                Message::Frame(_) => continue,
504            };
505            return Some(frame);
506        }
507    }
508}
509
510fn parse_frame(raw: &[u8]) -> WsResult<ServerMessage> {
511    let parsed = match std::str::from_utf8(raw) {
512        Ok(text) => parse_server_message(text),
513        Err(_) => serde_json::from_slice::<ServerMessage>(raw),
514    };
515    parsed.map_err(|error| {
516        WsClientError::UnreadableFrame(Box::new(UnreadableFrame::from_raw(raw, &error)))
517    })
518}