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    /// # Errors
27    /// Returns the original message when its variant is not this request's
28    /// response.
29    fn into_response(message: ServerMessage) -> Result<Self::Response, Box<ServerMessage>>;
30}
31
32/// A cursor-paged request. Pages run newest first by (second, id).
33pub trait PagedRequest: MakerDataRequest + Clone {
34    fn first_page(&mut self);
35    /// Points the cursor past the rows in `so_far`; `false` once nothing is left.
36    fn next_page(&mut self, so_far: &Self::Response) -> bool;
37    fn append(so_far: &mut Self::Response, page: Self::Response);
38}
39
40impl PagedRequest for GetMakerPositionsMessage {
41    fn first_page(&mut self) {
42        self.request_id = uuid::Uuid::new_v4();
43        self.limit = 500;
44        self.cursor = PageCursor::Start {};
45    }
46
47    fn next_page(&mut self, so_far: &MakerPositionsMessage) -> bool {
48        // the server orders a second's positions by their base58 PDA
49        let last = so_far
50            .positions
51            .iter()
52            .map(|p| (p.created_at, p.pda.to_base58(), p.pda))
53            .chain(
54                so_far
55                    .unrenderable
56                    .iter()
57                    .map(|p| (p.created_at, p.pda.to_base58(), p.pda)),
58            )
59            .min();
60        match (so_far.has_more, last) {
61            (true, Some((at, _, id))) => {
62                self.request_id = uuid::Uuid::new_v4();
63                self.cursor = PageCursor::After { at, id };
64                true
65            }
66            _ => false,
67        }
68    }
69
70    fn append(so_far: &mut MakerPositionsMessage, page: MakerPositionsMessage) {
71        so_far.positions.extend(page.positions);
72        so_far.unrenderable.extend(page.unrenderable);
73        so_far.has_more = page.has_more;
74    }
75}
76
77impl PagedRequest for GetMyTradesMessage {
78    fn first_page(&mut self) {
79        self.request_id = uuid::Uuid::new_v4();
80        self.limit = 200;
81        self.cursor = PageCursor::Start {};
82    }
83
84    fn next_page(&mut self, so_far: &MyTradesMessage) -> bool {
85        match (so_far.has_more, so_far.trades.last()) {
86            (true, Some(last)) => {
87                self.request_id = uuid::Uuid::new_v4();
88                self.cursor = PageCursor::After {
89                    at: last.confirmed_at,
90                    id: last.id,
91                };
92                true
93            }
94            _ => false,
95        }
96    }
97
98    fn append(so_far: &mut MyTradesMessage, page: MyTradesMessage) {
99        so_far.trades.extend(page.trades);
100        so_far.has_more = page.has_more;
101    }
102}
103
104/// `Live` is a single snapshot; only `History` pages.
105impl PagedRequest for GetMyQuotesMessage {
106    fn first_page(&mut self) {
107        self.request_id = uuid::Uuid::new_v4();
108        if let MakerQuoteScope::History { limit, cursor } = &mut self.scope {
109            *limit = 1000;
110            *cursor = PageCursor::Start {};
111        }
112    }
113
114    fn next_page(&mut self, so_far: &MyQuotesMessage) -> bool {
115        match (&mut self.scope, so_far.has_more, so_far.quotes.last()) {
116            (MakerQuoteScope::History { cursor, .. }, true, Some(last)) => {
117                *cursor = PageCursor::After {
118                    at: last.created_at,
119                    id: last.order_id,
120                };
121                self.request_id = uuid::Uuid::new_v4();
122                true
123            }
124            _ => false,
125        }
126    }
127
128    fn append(so_far: &mut MyQuotesMessage, page: MyQuotesMessage) {
129        so_far.quotes.extend(page.quotes);
130        so_far.has_more = page.has_more;
131    }
132}
133
134pub struct MakerResponse<T> {
135    payload: T,
136}
137
138impl<T> MakerResponse<T> {
139    #[must_use]
140    pub const fn payload(&self) -> &T {
141        &self.payload
142    }
143
144    #[must_use]
145    pub fn into_payload(self) -> T {
146        self.payload
147    }
148}
149
150impl<T> Deref for MakerResponse<T> {
151    type Target = T;
152
153    fn deref(&self) -> &Self::Target {
154        &self.payload
155    }
156}
157
158#[derive(Debug, thiserror::Error)]
159#[non_exhaustive]
160pub enum MakerDataRequestError {
161    #[error(transparent)]
162    Session(#[from] ManagedWsError),
163    #[error(transparent)]
164    Transport(#[from] SendAwaitError),
165    #[error("server rejected request {request_id}")]
166    Rejected {
167        request_id: uuid::Uuid,
168        response: Arc<ServerMessage>,
169    },
170    #[error("expected {expected}, received a different correlated response")]
171    UnexpectedResponse {
172        expected: &'static str,
173        response: Arc<ServerMessage>,
174    },
175}
176
177impl MakerDataRequestError {
178    #[must_use]
179    pub fn server_error(&self) -> Option<&ServerError> {
180        match self {
181            Self::Rejected { response, .. } => match &**response {
182                ServerMessage::RequestError(error) => Some(&error.error),
183                _ => None,
184            },
185            _ => None,
186        }
187    }
188}
189
190/// Maker data-plane client.
191#[derive(Clone)]
192pub struct MakerDataClient {
193    inner: ManagedWsHandle,
194}
195
196impl MakerDataClient {
197    pub fn spawn(config: ManagedWsConfig) -> Result<Self, MakerClientError> {
198        let config = Self::prepare_config(config)?;
199        Ok(Self {
200            inner: spawn_managed_ws(config)?,
201        })
202    }
203
204    fn prepare_config(config: ManagedWsConfig) -> Result<ManagedWsConfig, MakerClientError> {
205        if config.endpoint != MakerWsEndpoint::Data {
206            return Err(MakerClientError::WrongEndpoint {
207                client: "MakerDataClient",
208                required: MakerWsEndpoint::Data,
209                actual: config.endpoint,
210            });
211        }
212        Ok(config)
213    }
214
215    pub async fn request<R: MakerDataRequest>(
216        &self,
217        request: R,
218        timeout: Duration,
219    ) -> Result<MakerResponse<R::Response>, MakerDataRequestError> {
220        let message = self
221            .inner
222            .send_await_ready(request.into_message(), timeout)
223            .await?;
224        if let ServerMessage::RequestError(error) = &*message {
225            return Err(MakerDataRequestError::Rejected {
226                request_id: error.request_id,
227                response: message,
228            });
229        }
230        let message = match Arc::try_unwrap(message) {
231            Ok(owned) => owned,
232            Err(shared) => (*shared).clone(),
233        };
234        match R::into_response(message) {
235            Ok(payload) => Ok(MakerResponse { payload }),
236            Err(other) => Err(MakerDataRequestError::UnexpectedResponse {
237                expected: R::RESPONSE_TYPE,
238                response: Arc::from(other),
239            }),
240        }
241    }
242
243    /// Every page of `request` at the server's largest page size; its own
244    /// `limit` and cursor are ignored. Pages after the first are appended.
245    pub async fn all_pages<R: PagedRequest>(
246        &self,
247        mut request: R,
248        timeout: Duration,
249    ) -> Result<R::Response, MakerDataRequestError> {
250        request.first_page();
251        let mut all = self.request(request.clone(), timeout).await?.into_payload();
252        while request.next_page(&all) {
253            let page = self.request(request.clone(), timeout).await?.into_payload();
254            R::append(&mut all, page);
255        }
256        Ok(all)
257    }
258
259    /// Receive owner-direct `PositionUpdated`, `TradeExecuted` and
260    /// `MakerCapsUpdated` pushes alongside query responses. No subscription is
261    /// required. Owner pushes begin after `MmSummary` recovery; quote lifecycle
262    /// messages stay on the quote session. On reconnect or a stream gap, rebuild
263    /// authoritative state (for example with `maker_open_positions`).
264    pub fn subscribe_messages(&self) -> ManagedMessageReceiver {
265        self.inner.subscribe_messages()
266    }
267
268    pub fn subscribe_reference_changes(&self) -> ReferenceChangeReceiver {
269        self.inner.subscribe_reference_changes()
270    }
271
272    pub fn subscribe_events(&self) -> broadcast::Receiver<ManagedWsEvent> {
273        self.inner.subscribe_events()
274    }
275
276    #[must_use]
277    pub fn state(&self) -> ManagedWsState {
278        self.inner.state()
279    }
280
281    pub async fn wait_until_ready(&self) -> Result<u64, WaitUntilReadyError> {
282        self.inner.wait_until_ready().await
283    }
284
285    #[must_use]
286    pub const fn raw(&self) -> &ManagedWsHandle {
287        &self.inner
288    }
289
290    #[must_use]
291    pub fn into_raw(self) -> ManagedWsHandle {
292        self.inner
293    }
294
295    pub async fn close(&self) -> Result<(), ManagedWsError> {
296        self.inner.close().await
297    }
298}
299
300macro_rules! data_request {
301    ($request:ty, $client_variant:ident, $response:ty, $server_variant:ident) => {
302        impl private::Sealed for $request {}
303
304        impl MakerDataRequest for $request {
305            type Response = $response;
306
307            const RESPONSE_TYPE: &'static str = stringify!($server_variant);
308
309            fn into_message(self) -> ClientMessage {
310                ClientMessage::$client_variant(self)
311            }
312
313            fn into_response(message: ServerMessage) -> Result<Self::Response, Box<ServerMessage>> {
314                match message {
315                    ServerMessage::$server_variant(response) => Ok(response),
316                    other => Err(Box::new(other)),
317                }
318            }
319        }
320    };
321}
322
323data_request!(
324    GetActiveRfqsMessage,
325    GetActiveRfqs,
326    ActiveRfqsData,
327    ActiveRfqs
328);
329data_request!(GetMarketsMessage, GetMarkets, MarketsData, Markets);
330data_request!(
331    GetMarketDescriptorsMessage,
332    GetMarketDescriptors,
333    MarketDescriptorsData,
334    MarketDescriptors
335);
336data_request!(GetExpiriesMessage, GetExpiries, ExpiriesData, Expiries);
337data_request!(GetTokensMessage, GetTokens, TokensData, Tokens);
338data_request!(
339    GetIndicativePricesMessage,
340    GetIndicativePrices,
341    IndicativePricesMessage,
342    IndicativePrices
343);
344data_request!(
345    GetMakerPositionsMessage,
346    GetMakerPositions,
347    MakerPositionsMessage,
348    MakerPositions
349);
350data_request!(GetMyQuotesMessage, GetMyQuotes, MyQuotesMessage, MyQuotes);
351data_request!(
352    GetOrderStatusMessage,
353    GetOrderStatus,
354    OrderStatusMessage,
355    OrderStatus
356);
357data_request!(
358    GetMarketsForMakerMessage,
359    GetMarketsForMaker,
360    MakerMarketsMessage,
361    MakerMarkets
362);
363data_request!(GetTokenCapsMessage, GetTokenCaps, TokenCapsData, TokenCaps);
364data_request!(GetMyCapsMessage, GetMyCaps, MyCapsData, MyCaps);
365data_request!(CheckQuoteMessage, CheckQuote, QuoteCheckData, QuoteCheck);
366data_request!(GetMyTradesMessage, GetMyTrades, MyTradesMessage, MyTrades);
367data_request!(
368    GetEarnSummaryMessage,
369    GetEarnSummary,
370    EarnSummaryData,
371    EarnSummary
372);
373data_request!(GetMmSummaryMessage, GetMmSummary, MmSummaryData, MmSummary);
374data_request!(
375    GetTokenMarketsInfoMessage,
376    GetTokenMarketsInfo,
377    TokenMarketsInfoData,
378    TokenMarketsInfo
379);
380
381#[cfg(test)]
382mod tests {
383    use std::sync::Arc;
384
385    use super::*;
386    use crate::ws::managed::ManagedWsConfig;
387    use tokio::sync::mpsc::error::TryRecvError;
388
389    struct LeavesReadyWhileEncoding {
390        handle: ManagedWsHandle,
391        request_id: uuid::Uuid,
392    }
393
394    impl private::Sealed for LeavesReadyWhileEncoding {}
395
396    impl MakerDataRequest for LeavesReadyWhileEncoding {
397        type Response = MmSummaryData;
398
399        const RESPONSE_TYPE: &'static str = "MmSummary";
400
401        fn into_message(self) -> ClientMessage {
402            self.handle.inject_state(ManagedWsState::Authenticated {
403                connection_epoch: 7,
404            });
405            ClientMessage::GetMmSummary(GetMmSummaryMessage {
406                request_id: self.request_id,
407            })
408        }
409
410        fn into_response(message: ServerMessage) -> Result<Self::Response, Box<ServerMessage>> {
411            match message {
412                ServerMessage::MmSummary(response) => Ok(response),
413                other => Err(Box::new(other)),
414            }
415        }
416    }
417
418    fn config() -> ManagedWsConfig {
419        ManagedWsConfig::new(
420            "ws://localhost",
421            HelloData {
422                protocol_version: crate::WS_PROTOCOL_VERSION.to_string(),
423                on_disconnect: crate::ws::types::OnDisconnect::KeepQuotes,
424                client_name: "test".to_string(),
425                client_version: "test".to_string(),
426            },
427            Arc::new(crate::orders::BytesSigner::from_secret([1u8; 32])),
428            crate::Address32::new([76; 32]),
429        )
430    }
431
432    #[test]
433    fn data_client_rejects_quote_endpoint_before_spawning() {
434        assert!(matches!(
435            MakerDataClient::spawn(config()),
436            Err(MakerClientError::WrongEndpoint {
437                required: MakerWsEndpoint::Data,
438                actual: MakerWsEndpoint::Quote,
439                ..
440            })
441        ));
442    }
443
444    #[test]
445    fn trade_pages_continue_after_the_last_row_until_has_more_is_false() {
446        let page = |id: u128, confirmed_at: u64, has_more: bool| -> MyTradesMessage {
447            serde_json::from_value(serde_json::json!({
448                "request_id": uuid::Uuid::nil(),
449                "trades": [{
450                    "id": uuid::Uuid::from_u128(id), "rfq_id": uuid::Uuid::nil(),
451                    "market_pda": crate::Address32::new([1; 32]),
452                    "underlying_mint": crate::Address32::new([2; 32]), "underlying_symbol": "SOL",
453                    "underlying_decimals": 9,
454                    "quote_mint": crate::Address32::new([3; 32]), "quote_symbol": "USDC",
455                    "quote_decimals": 6,
456                    "terms": {"type": "covered_call", "strike": 100}, "side": "sell",
457                    "taker": crate::Address32::new([4; 32]), "quantity": 1, "price": 1,
458                    "tx_signature": "sig", "position_pda": null, "confirmed_at": confirmed_at
459                }],
460                "has_more": has_more
461            }))
462            .expect("trades page")
463        };
464        let mut request = GetMyTradesMessage {
465            request_id: uuid::Uuid::nil(),
466            limit: 1,
467            cursor: PageCursor::After {
468                at: std::time::UNIX_EPOCH,
469                id: uuid::Uuid::nil(),
470            },
471            market: QueryFilter::All {},
472        };
473        request.first_page();
474        assert_eq!(
475            (request.limit, &request.cursor),
476            (200, &PageCursor::Start {})
477        );
478
479        let mut so_far = page(2, 20, true);
480        assert!(request.next_page(&so_far));
481        assert_eq!(
482            request.cursor,
483            PageCursor::After {
484                at: std::time::UNIX_EPOCH + Duration::from_secs(20),
485                id: uuid::Uuid::from_u128(2),
486            }
487        );
488        GetMyTradesMessage::append(&mut so_far, page(1, 10, false));
489        assert_eq!(so_far.trades.len(), 2);
490        assert!(!request.next_page(&so_far));
491    }
492
493    #[test]
494    fn live_quotes_are_one_page() {
495        let mut request = GetMyQuotesMessage::default();
496        request.first_page();
497        assert_eq!(request.scope, MakerQuoteScope::Live {});
498        let so_far = MyQuotesMessage {
499            request_id: request.request_id,
500            quotes: Vec::new(),
501            has_more: true,
502        };
503        assert!(!request.next_page(&so_far));
504    }
505
506    #[test]
507    fn data_request_maps_to_one_typed_response() {
508        let request_id = uuid::Uuid::new_v4();
509        let request = GetActiveRfqsMessage { request_id };
510        assert!(matches!(
511            request.into_message(),
512            ClientMessage::GetActiveRfqs(GetActiveRfqsMessage {
513                request_id: actual
514            }) if actual == request_id
515        ));
516
517        let response = ServerMessage::ActiveRfqs(ActiveRfqsData {
518            request_id,
519            rfqs: Vec::new(),
520        });
521        assert_eq!(
522            <GetActiveRfqsMessage as MakerDataRequest>::into_response(response)
523                .expect("typed response")
524                .request_id,
525            request_id
526        );
527        assert!(matches!(
528            <GetActiveRfqsMessage as MakerDataRequest>::into_response(ServerMessage::RequireInvite),
529            Err(boxed) if matches!(*boxed, ServerMessage::RequireInvite)
530        ));
531    }
532
533    #[tokio::test]
534    async fn typed_request_does_not_enqueue_an_unbound_query_after_leaving_ready() {
535        let (handle, mut commands) = ManagedWsHandle::test_handle(1, 1);
536        handle.inject_state(ManagedWsState::Ready {
537            connection_epoch: 7,
538        });
539        let client = MakerDataClient {
540            inner: handle.clone(),
541        };
542
543        let result = tokio::select! {
544            result = client.request(
545                LeavesReadyWhileEncoding {
546                    handle,
547                    request_id: uuid::Uuid::new_v4(),
548                },
549                Duration::from_secs(1),
550            ) => result,
551            _ = commands.recv() => panic!("typed data request must not enqueue after leaving Ready"),
552        };
553
554        assert!(matches!(
555            result,
556            Err(MakerDataRequestError::Transport(SendAwaitError::NotReady))
557        ));
558        assert!(matches!(commands.try_recv(), Err(TryRecvError::Empty)));
559    }
560    #[tokio::test]
561    async fn data_client_exposes_all_owner_pushes() {
562        let (handle, _commands) = ManagedWsHandle::test_handle(1, 8);
563        let client = MakerDataClient {
564            inner: handle.clone(),
565        };
566        let mut messages = client.subscribe_messages();
567        for message in owner_pushes() {
568            let expected = serde_json::to_value(&message).unwrap();
569            handle.inject_message(message);
570            let received = messages.recv().await.unwrap();
571            assert_eq!(serde_json::to_value(received.message()).unwrap(), expected);
572        }
573    }
574
575    fn owner_pushes() -> Vec<ServerMessage> {
576        let caps = serde_json::json!({"positions": {"current": 1, "limit": 10}, "notional": [], "balances": [], "quote_liabilities": []});
577        [
578        serde_json::json!({"type": "MakerCapsUpdated", "data": {"caps_snapshot": caps, "reason": "lifecycle"}}),
579        serde_json::json!({"type": "PositionUpdated", "data": {
580            "position": {"revision": {"slot": 1, "transaction_index": 0, "instruction_index": 0}, "pda": "2PEfSGdYo17syw6vewKJceXbKHV481KWjXbo1x6g9E9D", "market": "11111111111111111111111111111111", "underlying_mint": "opdhLMLuFCTUEq3YCwdtgqfuhtMqdkHBsJf6YrHfUFF", "quote_mint": "Aa8SVbHR7yKKA6Zq2M2Yw9jGvNsZkMD6QKwCCCR7vgeA",
581                "terms": {"type": "covered_call", "strike": 1}, "side": "sell", "state": "open", "quantity": 2,
582                "price": 3, "total_premium": 6, "created_at": 1, "expiry_ts": 2,
583                "underlying_decimals": 9, "quote_decimals": 6},
584            "update_type": "created"
585        }}),
586        serde_json::json!({"type": "TradeExecuted", "data": {"trade": {
587            "id": "00000000-0000-0000-0000-000000000001", "market": "11111111111111111111111111111111", "terms": {"type": "covered_call", "strike": 1}, "side": "sell",
588            "quantity": 2, "price": 3, "taker": "GKwe8PT8YLdJiztcH7TpVkf7GVHFtHu4tNW9x5woDp6T", "maker": "7rJSLqoFCCqN39ztKiotFqaxwxEtycG4Fm8XzF4Bvx2M", "tx_signature": "s", "executed_at": 1
589        }}}),
590    ].into_iter().map(|value| serde_json::from_value(value).unwrap()).collect()
591    }
592}