Skip to main content

acta_maker_sdk/ws/maker/
data.rs

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
19/// Request with a statically known data-plane response.
20pub 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    /// # Errors
35    /// Returns the original message when its variant is not this request's
36    /// response.
37    fn into_response(message: ServerMessage) -> Result<Self::Response, Box<ServerMessage>>;
38}
39
40/// A list request using the common opaque continuation contract.
41pub 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/// Maker data-plane client.
247#[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    /// Fetch the complete response. A list request follows every page, so use
272    /// `request_page` to stop early.
273    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    /// Fetch exactly one page. Use `request_all` for a complete collection.
282    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    /// Every page of `request` at the server's largest page size; its own
311    /// `limit` and cursor are ignored. Pages after the first are appended.
312    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    /// Receive owner-direct `PositionUpdated`, `TradeExecuted` and
335    /// `MakerCapsUpdated` pushes alongside query responses. No subscription is
336    /// required. Owner pushes begin after `MmSummary` recovery; quote lifecycle
337    /// messages stay on the quote session. On reconnect or a stream gap, rebuild
338    /// authoritative state (for example with `maker_open_positions`).
339    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}