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
54pub struct WsClient {
62 stream: Socket,
63 server_close: Arc<OnceLock<ServerClose>>,
64 stream_label: &'static str,
65}
66
67pub(crate) struct WsReader {
70 stream: SplitStream<Socket>,
71 server_close: Arc<OnceLock<ServerClose>>,
72 stream_label: &'static str,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct ServerClose {
78 pub code: u16,
79 pub reason: String,
80}
81
82impl ServerClose {
83 #[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 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 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 #[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 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 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}