1use std::ops::Deref;
2use std::sync::Arc;
3use std::time::Duration;
4
5use crate::ws::managed::{
6 MakerWsEndpoint, ManagedMessageReceiver, ManagedWsConfig, ManagedWsError, ManagedWsEvent,
7 ManagedWsHandle, ManagedWsState, ReferenceChangeReceiver, SendAwaitError, WaitUntilReadyError,
8 spawn_managed_ws,
9};
10use crate::ws::types::*;
11use tokio::sync::broadcast;
12
13use super::MakerClientError;
14
15mod private {
16 pub trait Sealed {}
17}
18
19pub trait MakerDataRequest: private::Sealed + Sized {
21 type Response;
22
23 const RESPONSE_TYPE: &'static str;
24
25 fn into_message(self) -> ClientMessage;
26 fn fetch(
27 self,
28 client: &MakerDataClient,
29 timeout: Duration,
30 ) -> impl std::future::Future<Output = Result<MakerResponse<Self::Response>, MakerDataRequestError>>
31 {
32 client.request_page(self, timeout)
33 }
34 fn into_response(message: ServerMessage) -> Result<Self::Response, Box<ServerMessage>>;
38}
39
40pub trait PagedRequest: MakerDataRequest + Clone {
42 fn first_page(&mut self);
43 fn next_page(&mut self, page: &Self::Response) -> bool;
44 fn append(all: &mut Self::Response, page: Self::Response);
45 fn has_more(page: &Self::Response) -> bool;
46 fn cursor(page: &Self::Response) -> Option<&str>;
47 fn request_cursor(&mut self, cursor: Cursor);
48}
49
50macro_rules! paged_request {
51 ($request:ty, $response:ty, $($field:ident).+ $(, $($rest:ident).+)* $(,)?) => {
52 impl PagedRequest for $request {
53 fn first_page(&mut self) {
54 self.request_id = uuid::Uuid::new_v4();
55 self.limit = 200;
56 self.cursor = Cursor::Start {};
57 }
58 fn next_page(&mut self, page: &$response) -> bool {
59 let Some(id) = &page.next_cursor else { return false; };
60 let cursor = Cursor::After { id: id.clone() };
61 if id.is_empty() || cursor == self.cursor { return false; }
62 self.cursor = cursor;
63 self.request_id = uuid::Uuid::new_v4();
64 true
65 }
66 fn append(all: &mut $response, page: $response) {
67 all.$($field).+.extend(page.$($field).+);
68 $(all.$($rest).+.extend(page.$($rest).+);)*
69 all.next_cursor = page.next_cursor;
70 }
71 fn has_more(page: &$response) -> bool { page.next_cursor.is_some() }
72 fn cursor(page: &$response) -> Option<&str> { page.next_cursor.as_deref() }
73 fn request_cursor(&mut self, cursor: Cursor) { self.cursor = cursor; self.request_id = uuid::Uuid::new_v4(); }
74 }
75 };
76}
77
78paged_request!(
79 GetMakerPositionsMessage,
80 MakerPositionsMessage,
81 positions,
82 unrenderable
83);
84paged_request!(GetMyTradesMessage, MyTradesMessage, trades);
85paged_request!(GetMyQuotesMessage, MyQuotesMessage, quotes);
86paged_request!(GetMarketsMessage, MarketsData, markets);
87paged_request!(GetMarketDescriptorsMessage, MarketDescriptorsData, markets);
88paged_request!(GetExpiriesMessage, ExpiriesData, expiries_ts);
89paged_request!(GetTokensMessage, TokensData, tokens);
90paged_request!(GetActiveRfqsMessage, ActiveRfqsData, rfqs);
91paged_request!(GetMyActiveRfqsMessage, MyActiveRfqsData, rfqs);
92paged_request!(GetPositionsMessage, PositionsData, positions);
93paged_request!(GetPositionSummaryMessage, PositionSummaryData, summaries);
94paged_request!(GetIndicativePricesMessage, IndicativePricesMessage, strikes);
95paged_request!(GetMarketsForMakerMessage, MakerMarketsMessage, markets);
96paged_request!(
97 GetTokenCapsMessage,
98 TokenCapsData,
99 tokens,
100 markets,
101 quotes,
102 quote_liabilities
103);
104paged_request!(
105 GetMyCapsMessage,
106 MyCapsData,
107 notional,
108 balances,
109 quote_liabilities
110);
111paged_request!(GetEarnSummaryMessage, EarnSummaryData, assets);
112paged_request!(
113 GetSubscriptionsMessage,
114 SubscriptionsMessage,
115 channels,
116 underlying_mints,
117 quote_mints
118);
119paged_request!(
120 GetMmSummaryMessage,
121 MmSummaryData,
122 positions,
123 unrenderable_positions,
124 active_quotes,
125 markets,
126 tokens,
127 caps.notional,
128 caps.balances,
129 caps.quote_liabilities
130);
131
132impl PagedRequest for GetTokenMarketsInfoMessage {
133 fn first_page(&mut self) {
134 self.request_id = uuid::Uuid::new_v4();
135 self.limit = 200;
136 self.cursor = Cursor::Start {};
137 }
138 fn next_page(&mut self, page: &TokenMarketsInfoData) -> bool {
139 let Some(id) = &page.next_cursor else {
140 return false;
141 };
142 let cursor = Cursor::After { id: id.clone() };
143 if id.is_empty() || cursor == self.cursor {
144 return false;
145 }
146 self.cursor = cursor;
147 self.request_id = uuid::Uuid::new_v4();
148 true
149 }
150 fn has_more(page: &TokenMarketsInfoData) -> bool {
151 page.next_cursor.is_some()
152 }
153 fn cursor(page: &TokenMarketsInfoData) -> Option<&str> {
154 page.next_cursor.as_deref()
155 }
156 fn request_cursor(&mut self, cursor: Cursor) {
157 self.cursor = cursor;
158 self.request_id = uuid::Uuid::new_v4();
159 }
160 fn append(all: &mut TokenMarketsInfoData, page: TokenMarketsInfoData) {
161 for market in page.markets {
162 if let Some(existing) = all
163 .markets
164 .iter_mut()
165 .find(|m| m.market_pda == market.market_pda)
166 {
167 for indicative in market.indicatives {
168 if let Some(current) = existing
169 .indicatives
170 .iter_mut()
171 .find(|i| i.position_type == indicative.position_type)
172 {
173 current.strikes.extend(indicative.strikes);
174 } else {
175 existing.indicatives.push(indicative);
176 }
177 }
178 } else {
179 all.markets.push(market);
180 }
181 }
182 all.next_cursor = page.next_cursor;
183 }
184}
185
186pub struct MakerResponse<T> {
187 payload: T,
188}
189
190impl<T> MakerResponse<T> {
191 #[must_use]
192 pub const fn payload(&self) -> &T {
193 &self.payload
194 }
195
196 #[must_use]
197 pub fn into_payload(self) -> T {
198 self.payload
199 }
200}
201
202impl<T> Deref for MakerResponse<T> {
203 type Target = T;
204
205 fn deref(&self) -> &Self::Target {
206 &self.payload
207 }
208}
209
210#[derive(Debug, thiserror::Error)]
211#[non_exhaustive]
212pub enum MakerDataRequestError {
213 #[error("page claims more rows but provides no advancing cursor")]
214 InvalidPage,
215 #[error(transparent)]
216 Continuation(#[from] crate::paging::InvalidPage),
217 #[error(transparent)]
218 Session(#[from] ManagedWsError),
219 #[error(transparent)]
220 Transport(#[from] SendAwaitError),
221 #[error("server rejected request {request_id}")]
222 Rejected {
223 request_id: uuid::Uuid,
224 response: Arc<ServerMessage>,
225 },
226 #[error("expected {expected}, received a different correlated response")]
227 UnexpectedResponse {
228 expected: &'static str,
229 response: Arc<ServerMessage>,
230 },
231}
232
233impl MakerDataRequestError {
234 #[must_use]
235 pub fn server_error(&self) -> Option<&ServerError> {
236 match self {
237 Self::Rejected { response, .. } => match &**response {
238 ServerMessage::RequestError(error) => Some(&error.error),
239 _ => None,
240 },
241 _ => None,
242 }
243 }
244}
245
246#[derive(Clone)]
248pub struct MakerDataClient {
249 inner: ManagedWsHandle,
250}
251
252impl MakerDataClient {
253 pub fn spawn(config: ManagedWsConfig) -> Result<Self, MakerClientError> {
254 let config = Self::prepare_config(config)?;
255 Ok(Self {
256 inner: spawn_managed_ws(config)?,
257 })
258 }
259
260 fn prepare_config(config: ManagedWsConfig) -> Result<ManagedWsConfig, MakerClientError> {
261 if config.endpoint != MakerWsEndpoint::Data {
262 return Err(MakerClientError::WrongEndpoint {
263 client: "MakerDataClient",
264 required: MakerWsEndpoint::Data,
265 actual: config.endpoint,
266 });
267 }
268 Ok(config)
269 }
270
271 pub async fn request_all<R: MakerDataRequest>(
274 &self,
275 request: R,
276 timeout: Duration,
277 ) -> Result<MakerResponse<R::Response>, MakerDataRequestError> {
278 request.fetch(self, timeout).await
279 }
280
281 pub async fn request_page<R: MakerDataRequest>(
283 &self,
284 request: R,
285 timeout: Duration,
286 ) -> Result<MakerResponse<R::Response>, MakerDataRequestError> {
287 let message = self
288 .inner
289 .send_await_ready(request.into_message(), timeout)
290 .await?;
291 if let ServerMessage::RequestError(error) = &*message {
292 return Err(MakerDataRequestError::Rejected {
293 request_id: error.request_id,
294 response: message,
295 });
296 }
297 let message = match Arc::try_unwrap(message) {
298 Ok(owned) => owned,
299 Err(shared) => (*shared).clone(),
300 };
301 match R::into_response(message) {
302 Ok(payload) => Ok(MakerResponse { payload }),
303 Err(other) => Err(MakerDataRequestError::UnexpectedResponse {
304 expected: R::RESPONSE_TYPE,
305 response: Arc::from(other),
306 }),
307 }
308 }
309
310 pub async fn all_pages<R: PagedRequest>(
313 &self,
314 mut request: R,
315 timeout: Duration,
316 ) -> Result<R::Response, MakerDataRequestError> {
317 request.first_page();
318 crate::paging::fetch_all(
319 |cursor| {
320 let mut next = request.clone();
321 next.request_cursor(cursor);
322 async move {
323 self.request_page(next, timeout)
324 .await
325 .map(MakerResponse::into_payload)
326 }
327 },
328 R::cursor,
329 R::append,
330 )
331 .await
332 }
333
334 pub fn subscribe_messages(&self) -> ManagedMessageReceiver {
340 self.inner.subscribe_messages()
341 }
342
343 pub fn subscribe_reference_changes(&self) -> ReferenceChangeReceiver {
344 self.inner.subscribe_reference_changes()
345 }
346
347 pub fn subscribe_events(&self) -> broadcast::Receiver<ManagedWsEvent> {
348 self.inner.subscribe_events()
349 }
350
351 #[must_use]
352 pub fn state(&self) -> ManagedWsState {
353 self.inner.state()
354 }
355
356 pub async fn wait_until_ready(&self) -> Result<u64, WaitUntilReadyError> {
357 self.inner.wait_until_ready().await
358 }
359
360 #[must_use]
361 pub const fn raw(&self) -> &ManagedWsHandle {
362 &self.inner
363 }
364
365 #[must_use]
366 pub fn into_raw(self) -> ManagedWsHandle {
367 self.inner
368 }
369
370 pub async fn close(&self) -> Result<(), ManagedWsError> {
371 self.inner.close().await
372 }
373}
374
375macro_rules! paged_fetch {
376 (paged) => {
377 async fn fetch(
378 self,
379 client: &MakerDataClient,
380 timeout: Duration,
381 ) -> Result<MakerResponse<Self::Response>, MakerDataRequestError> {
382 client
383 .all_pages(self, timeout)
384 .await
385 .map(|payload| MakerResponse { payload })
386 }
387 };
388}
389
390macro_rules! data_request {
391 ($request:ty, $client_variant:ident, $response:ty, $server_variant:ident $(, $paged:ident)?) => {
392 impl private::Sealed for $request {}
393
394 impl MakerDataRequest for $request {
395 type Response = $response;
396
397 $(paged_fetch!($paged);)?
398
399 const RESPONSE_TYPE: &'static str = stringify!($server_variant);
400
401 fn into_message(self) -> ClientMessage {
402 ClientMessage::$client_variant(self)
403 }
404
405 fn into_response(message: ServerMessage) -> Result<Self::Response, Box<ServerMessage>> {
406 match message {
407 ServerMessage::$server_variant(response) => Ok(response),
408 other => Err(Box::new(other)),
409 }
410 }
411 }
412 };
413}
414
415data_request!(
416 GetActiveRfqsMessage,
417 GetActiveRfqs,
418 ActiveRfqsData,
419 ActiveRfqs,
420 paged
421);
422data_request!(GetMarketsMessage, GetMarkets, MarketsData, Markets, paged);
423data_request!(
424 GetMarketDescriptorsMessage,
425 GetMarketDescriptors,
426 MarketDescriptorsData,
427 MarketDescriptors,
428 paged
429);
430data_request!(
431 GetExpiriesMessage,
432 GetExpiries,
433 ExpiriesData,
434 Expiries,
435 paged
436);
437data_request!(GetTokensMessage, GetTokens, TokensData, Tokens, paged);
438data_request!(
439 GetIndicativePricesMessage,
440 GetIndicativePrices,
441 IndicativePricesMessage,
442 IndicativePrices,
443 paged
444);
445data_request!(
446 GetMakerPositionsMessage,
447 GetMakerPositions,
448 MakerPositionsMessage,
449 MakerPositions,
450 paged
451);
452data_request!(
453 GetMyQuotesMessage,
454 GetMyQuotes,
455 MyQuotesMessage,
456 MyQuotes,
457 paged
458);
459data_request!(
460 GetOrderStatusMessage,
461 GetOrderStatus,
462 OrderStatusMessage,
463 OrderStatus
464);
465data_request!(
466 GetMarketsForMakerMessage,
467 GetMarketsForMaker,
468 MakerMarketsMessage,
469 MakerMarkets,
470 paged
471);
472data_request!(
473 GetTokenCapsMessage,
474 GetTokenCaps,
475 TokenCapsData,
476 TokenCaps,
477 paged
478);
479data_request!(GetMyCapsMessage, GetMyCaps, MyCapsData, MyCaps, paged);
480data_request!(CheckQuoteMessage, CheckQuote, QuoteCheckData, QuoteCheck);
481data_request!(
482 GetMyTradesMessage,
483 GetMyTrades,
484 MyTradesMessage,
485 MyTrades,
486 paged
487);
488data_request!(
489 GetEarnSummaryMessage,
490 GetEarnSummary,
491 EarnSummaryData,
492 EarnSummary,
493 paged
494);
495data_request!(
496 GetMmSummaryMessage,
497 GetMmSummary,
498 MmSummaryData,
499 MmSummary,
500 paged
501);
502data_request!(
503 GetTokenMarketsInfoMessage,
504 GetTokenMarketsInfo,
505 TokenMarketsInfoData,
506 TokenMarketsInfo,
507 paged
508);
509
510data_request!(
511 GetPositionsMessage,
512 GetPositions,
513 PositionsData,
514 Positions,
515 paged
516);
517data_request!(
518 GetPositionSummaryMessage,
519 GetPositionSummary,
520 PositionSummaryData,
521 PositionSummary,
522 paged
523);
524data_request!(
525 GetMyActiveRfqsMessage,
526 GetMyActiveRfqs,
527 MyActiveRfqsData,
528 MyActiveRfqs,
529 paged
530);
531data_request!(
532 GetSubscriptionsMessage,
533 GetSubscriptions,
534 SubscriptionsMessage,
535 Subscriptions,
536 paged
537);
538
539#[cfg(test)]
540mod tests {
541 use std::sync::Arc;
542
543 use super::*;
544 use crate::ws::managed::ManagedWsConfig;
545 use tokio::sync::mpsc::error::TryRecvError;
546
547 struct LeavesReadyWhileEncoding {
548 handle: ManagedWsHandle,
549 request_id: uuid::Uuid,
550 }
551
552 impl private::Sealed for LeavesReadyWhileEncoding {}
553
554 impl MakerDataRequest for LeavesReadyWhileEncoding {
555 type Response = MmSummaryData;
556
557 const RESPONSE_TYPE: &'static str = "MmSummary";
558
559 fn into_message(self) -> ClientMessage {
560 self.handle.inject_state(ManagedWsState::Authenticated {
561 connection_epoch: 7,
562 });
563 ClientMessage::GetMmSummary(GetMmSummaryMessage {
564 limit: 200,
565 cursor: crate::ws::types::Cursor::Start {},
566
567 request_id: self.request_id,
568 })
569 }
570
571 fn into_response(message: ServerMessage) -> Result<Self::Response, Box<ServerMessage>> {
572 match message {
573 ServerMessage::MmSummary(response) => Ok(response),
574 other => Err(Box::new(other)),
575 }
576 }
577 }
578
579 fn config() -> ManagedWsConfig {
580 ManagedWsConfig::new(
581 "ws://localhost",
582 HelloData {
583 protocol_version: crate::WS_PROTOCOL_VERSION.to_string(),
584 on_disconnect: crate::ws::types::OnDisconnect::KeepQuotes,
585 client_name: "test".to_string(),
586 client_version: "test".to_string(),
587 },
588 Arc::new(crate::orders::BytesSigner::from_secret([1u8; 32])),
589 crate::Address32::new([76; 32]),
590 )
591 }
592
593 #[test]
594 fn data_client_rejects_quote_endpoint_before_spawning() {
595 assert!(matches!(
596 MakerDataClient::spawn(config()),
597 Err(MakerClientError::WrongEndpoint {
598 required: MakerWsEndpoint::Data,
599 actual: MakerWsEndpoint::Quote,
600 ..
601 })
602 ));
603 }
604
605 #[test]
606 fn continuations_use_the_server_key_even_without_renderable_rows() {
607 let mut request = GetMyQuotesMessage::default();
608 request.first_page();
609 let mut all = MyQuotesMessage {
610 request_id: request.request_id,
611 quotes: Vec::new(),
612 next_cursor: Some("opaque".into()),
613 };
614 assert!(request.next_page(&all));
615 assert_eq!(
616 request.cursor,
617 Cursor::After {
618 id: "opaque".into()
619 }
620 );
621 assert!(!request.next_page(&all));
622 GetMyQuotesMessage::append(
623 &mut all,
624 MyQuotesMessage {
625 request_id: request.request_id,
626 quotes: Vec::new(),
627 next_cursor: None,
628 },
629 );
630 assert!(!GetMyQuotesMessage::has_more(&all));
631 }
632
633 #[test]
634 fn empty_continuation_is_rejected() {
635 let mut request = GetMyQuotesMessage::default();
636 let page = MyQuotesMessage {
637 request_id: request.request_id,
638 quotes: Vec::new(),
639 next_cursor: Some(String::new()),
640 };
641 assert!(!request.next_page(&page));
642 }
643
644 #[test]
645 fn data_request_maps_to_one_typed_response() {
646 let request_id = uuid::Uuid::new_v4();
647 let request = GetActiveRfqsMessage {
648 limit: 200,
649 cursor: crate::ws::types::Cursor::Start {},
650 request_id,
651 };
652 assert!(matches!(
653 request.into_message(),
654 ClientMessage::GetActiveRfqs(GetActiveRfqsMessage {
655 limit: 200,
656 cursor: crate::ws::types::Cursor::Start {},
657
658 request_id: actual
659 }) if actual == request_id
660 ));
661
662 let response = ServerMessage::ActiveRfqs(ActiveRfqsData {
663 next_cursor: None,
664
665 request_id,
666 rfqs: Vec::new(),
667 });
668 assert_eq!(
669 <GetActiveRfqsMessage as MakerDataRequest>::into_response(response)
670 .expect("typed response")
671 .request_id,
672 request_id
673 );
674 assert!(matches!(
675 <GetActiveRfqsMessage as MakerDataRequest>::into_response(ServerMessage::RequireInvite),
676 Err(boxed) if matches!(*boxed, ServerMessage::RequireInvite)
677 ));
678 }
679
680 #[tokio::test]
681 async fn typed_request_does_not_enqueue_an_unbound_query_after_leaving_ready() {
682 let (handle, mut commands) = ManagedWsHandle::test_handle(1, 1);
683 handle.inject_state(ManagedWsState::Ready {
684 connection_epoch: 7,
685 });
686 let client = MakerDataClient {
687 inner: handle.clone(),
688 };
689
690 let result = tokio::select! {
691 result = client.request_all(
692 LeavesReadyWhileEncoding {
693 handle,
694 request_id: uuid::Uuid::new_v4(),
695 },
696 Duration::from_secs(1),
697 ) => result,
698 _ = commands.recv() => panic!("typed data request must not enqueue after leaving Ready"),
699 };
700
701 assert!(matches!(
702 result,
703 Err(MakerDataRequestError::Transport(SendAwaitError::NotReady))
704 ));
705 assert!(matches!(commands.try_recv(), Err(TryRecvError::Empty)));
706 }
707 #[tokio::test]
708 async fn data_client_exposes_all_owner_pushes() {
709 let (handle, _commands) = ManagedWsHandle::test_handle(1, 8);
710 let client = MakerDataClient {
711 inner: handle.clone(),
712 };
713 let mut messages = client.subscribe_messages();
714 for message in owner_pushes() {
715 let expected = serde_json::to_value(&message).unwrap();
716 handle.inject_message(message);
717 let received = messages.recv().await.unwrap();
718 assert_eq!(serde_json::to_value(received.message()).unwrap(), expected);
719 }
720 }
721
722 fn owner_pushes() -> Vec<ServerMessage> {
723 let caps = serde_json::json!({"positions": {"current": 1, "limit": 10}, "notional": [], "balances": [], "quote_liabilities": []});
724 [
725 serde_json::json!({"type": "MakerCapsUpdated", "data": {"caps_snapshot": caps, "reason": "lifecycle"}}),
726 serde_json::json!({"type": "PositionUpdated", "data": {
727 "position": {"revision": {"slot": 1, "transaction_index": 0, "instruction_index": 0}, "pda": "2PEfSGdYo17syw6vewKJceXbKHV481KWjXbo1x6g9E9D", "market": "11111111111111111111111111111111", "underlying_mint": "opdhLMLuFCTUEq3YCwdtgqfuhtMqdkHBsJf6YrHfUFF", "quote_mint": "Aa8SVbHR7yKKA6Zq2M2Yw9jGvNsZkMD6QKwCCCR7vgeA",
728 "terms": {"type": "covered_call", "strike": 1}, "side": "sell", "state": "open", "quantity": 2,
729 "price": 3, "total_premium": 6, "created_at": 1, "expiry_ts": 2,
730 "underlying_decimals": 9, "quote_decimals": 6,
731 "underlying_price": {"type": "available", "price": 150, "published_at": 1}},
732 "update_type": "created"
733 }}),
734 serde_json::json!({"type": "TradeExecuted", "data": {"trade": {
735 "id": "00000000-0000-0000-0000-000000000001", "market": "11111111111111111111111111111111", "terms": {"type": "covered_call", "strike": 1}, "side": "sell",
736 "quantity": 2, "price": 3, "taker": "GKwe8PT8YLdJiztcH7TpVkf7GVHFtHu4tNW9x5woDp6T", "maker": "7rJSLqoFCCqN39ztKiotFqaxwxEtycG4Fm8XzF4Bvx2M", "tx_signature": "s", "executed_at": 1
737 }}}),
738 ].into_iter().map(|value| serde_json::from_value(value).unwrap()).collect()
739 }
740}