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
32pub struct MakerResponse<T> {
33    payload: T,
34}
35
36impl<T> MakerResponse<T> {
37    #[must_use]
38    pub const fn payload(&self) -> &T {
39        &self.payload
40    }
41
42    #[must_use]
43    pub fn into_payload(self) -> T {
44        self.payload
45    }
46}
47
48impl<T> Deref for MakerResponse<T> {
49    type Target = T;
50
51    fn deref(&self) -> &Self::Target {
52        &self.payload
53    }
54}
55
56#[derive(Debug, thiserror::Error)]
57#[non_exhaustive]
58pub enum MakerDataRequestError {
59    #[error(transparent)]
60    Session(#[from] ManagedWsError),
61    #[error(transparent)]
62    Transport(#[from] SendAwaitError),
63    #[error("server rejected request {request_id}")]
64    Rejected {
65        request_id: uuid::Uuid,
66        response: Arc<ServerMessage>,
67    },
68    #[error("expected {expected}, received a different correlated response")]
69    UnexpectedResponse {
70        expected: &'static str,
71        response: Arc<ServerMessage>,
72    },
73}
74
75impl MakerDataRequestError {
76    #[must_use]
77    pub fn server_error(&self) -> Option<&ServerError> {
78        match self {
79            Self::Rejected { response, .. } => match &**response {
80                ServerMessage::RequestError(error) => Some(&error.error),
81                _ => None,
82            },
83            _ => None,
84        }
85    }
86}
87
88/// The server's `GetMakerPositions` page cap.
89const MAKER_POSITIONS_PAGE_LIMIT: u32 = 500;
90
91/// Maker data-plane client.
92#[derive(Clone)]
93pub struct MakerDataClient {
94    inner: ManagedWsHandle,
95}
96
97impl MakerDataClient {
98    pub fn spawn(config: ManagedWsConfig) -> Result<Self, MakerClientError> {
99        let config = Self::prepare_config(config)?;
100        Ok(Self {
101            inner: spawn_managed_ws(config)?,
102        })
103    }
104
105    fn prepare_config(config: ManagedWsConfig) -> Result<ManagedWsConfig, MakerClientError> {
106        if config.endpoint != MakerWsEndpoint::Data {
107            return Err(MakerClientError::WrongEndpoint {
108                client: "MakerDataClient",
109                required: MakerWsEndpoint::Data,
110                actual: config.endpoint,
111            });
112        }
113        Ok(config)
114    }
115
116    pub async fn request<R: MakerDataRequest>(
117        &self,
118        request: R,
119        timeout: Duration,
120    ) -> Result<MakerResponse<R::Response>, MakerDataRequestError> {
121        let message = self
122            .inner
123            .send_await_ready(request.into_message(), timeout)
124            .await?;
125        if let ServerMessage::RequestError(error) = &*message {
126            return Err(MakerDataRequestError::Rejected {
127                request_id: error.request_id,
128                response: message,
129            });
130        }
131        let message = match Arc::try_unwrap(message) {
132            Ok(owned) => owned,
133            Err(shared) => (*shared).clone(),
134        };
135        match R::into_response(message) {
136            Ok(payload) => Ok(MakerResponse { payload }),
137            Err(other) => Err(MakerDataRequestError::UnexpectedResponse {
138                expected: R::RESPONSE_TYPE,
139                response: Arc::from(other),
140            }),
141        }
142    }
143
144    /// Every position matching `filter`, paged at the server maximum; the
145    /// filter's `limit` and cursor are ignored. Use after an `MmSummary` with
146    /// `positions_has_more`.
147    pub async fn all_maker_positions(
148        &self,
149        mut filter: GetMakerPositionsMessage,
150        timeout: Duration,
151    ) -> Result<MakerPositionsMessage, MakerDataRequestError> {
152        let mut all = MakerPositionsMessage {
153            request_id: filter.request_id,
154            positions: Vec::new(),
155            unrenderable: Vec::new(),
156            has_more: false,
157        };
158        filter.limit = Some(MAKER_POSITIONS_PAGE_LIMIT);
159        filter.cursor = None;
160        filter.cursor_id = None;
161        loop {
162            filter.request_id = uuid::Uuid::new_v4();
163            let page = self.request(filter.clone(), timeout).await?.into_payload();
164            // pages run (created_at second, pda) descending, across both lists
165            let last = page
166                .positions
167                .iter()
168                .map(|p| (p.created_at, p.pda.as_str()))
169                .chain(
170                    page.unrenderable
171                        .iter()
172                        .map(|p| (p.created_at, p.pda.as_str())),
173                )
174                .min()
175                .map(|(created_at, pda)| (created_at, pda.to_owned()));
176            all.positions.extend(page.positions);
177            all.unrenderable.extend(page.unrenderable);
178            match (page.has_more, last) {
179                (true, Some((created_at, pda))) => {
180                    filter.cursor = Some(created_at);
181                    filter.cursor_id = Some(pda);
182                }
183                _ => return Ok(all),
184            }
185        }
186    }
187
188    /// Receive owner-direct `PositionUpdated`, `TradeExecuted` and
189    /// `MakerCapsUpdated` pushes alongside query responses. No subscription is
190    /// required. Owner pushes begin after `MmSummary` recovery; quote lifecycle
191    /// messages stay on the quote session. On reconnect or a stream gap, rebuild
192    /// authoritative state (for example with `maker_open_positions`).
193    pub fn subscribe_messages(&self) -> ManagedMessageReceiver {
194        self.inner.subscribe_messages()
195    }
196
197    pub fn subscribe_reference_changes(&self) -> ReferenceChangeReceiver {
198        self.inner.subscribe_reference_changes()
199    }
200
201    pub fn subscribe_events(&self) -> broadcast::Receiver<ManagedWsEvent> {
202        self.inner.subscribe_events()
203    }
204
205    #[must_use]
206    pub fn state(&self) -> ManagedWsState {
207        self.inner.state()
208    }
209
210    pub async fn wait_until_ready(&self) -> Result<u64, WaitUntilReadyError> {
211        self.inner.wait_until_ready().await
212    }
213
214    #[must_use]
215    pub const fn raw(&self) -> &ManagedWsHandle {
216        &self.inner
217    }
218
219    #[must_use]
220    pub fn into_raw(self) -> ManagedWsHandle {
221        self.inner
222    }
223
224    pub async fn close(&self) -> Result<(), ManagedWsError> {
225        self.inner.close().await
226    }
227}
228
229macro_rules! data_request {
230    ($request:ty, $client_variant:ident, $response:ty, $server_variant:ident) => {
231        impl private::Sealed for $request {}
232
233        impl MakerDataRequest for $request {
234            type Response = $response;
235
236            const RESPONSE_TYPE: &'static str = stringify!($server_variant);
237
238            fn into_message(self) -> ClientMessage {
239                ClientMessage::$client_variant(self)
240            }
241
242            fn into_response(message: ServerMessage) -> Result<Self::Response, Box<ServerMessage>> {
243                match message {
244                    ServerMessage::$server_variant(response) => Ok(response),
245                    other => Err(Box::new(other)),
246                }
247            }
248        }
249    };
250}
251
252data_request!(
253    GetActiveRfqsMessage,
254    GetActiveRfqs,
255    ActiveRfqsData,
256    ActiveRfqs
257);
258data_request!(GetMarketsMessage, GetMarkets, MarketsData, Markets);
259data_request!(
260    GetMarketDescriptorsMessage,
261    GetMarketDescriptors,
262    MarketDescriptorsData,
263    MarketDescriptors
264);
265data_request!(GetExpiriesMessage, GetExpiries, ExpiriesData, Expiries);
266data_request!(GetTokensMessage, GetTokens, TokensData, Tokens);
267data_request!(
268    GetIndicativePricesMessage,
269    GetIndicativePrices,
270    IndicativePricesMessage,
271    IndicativePrices
272);
273data_request!(
274    GetMakerPositionsMessage,
275    GetMakerPositions,
276    MakerPositionsMessage,
277    MakerPositions
278);
279data_request!(GetMyQuotesMessage, GetMyQuotes, MyQuotesMessage, MyQuotes);
280data_request!(
281    GetOrderStatusMessage,
282    GetOrderStatus,
283    OrderStatusMessage,
284    OrderStatus
285);
286data_request!(
287    GetMarketsForMakerMessage,
288    GetMarketsForMaker,
289    MakerMarketsMessage,
290    MakerMarkets
291);
292data_request!(GetTokenCapsMessage, GetTokenCaps, TokenCapsData, TokenCaps);
293data_request!(GetMyCapsMessage, GetMyCaps, MyCapsData, MyCaps);
294data_request!(CheckQuoteMessage, CheckQuote, QuoteCheckData, QuoteCheck);
295data_request!(GetMyTradesMessage, GetMyTrades, MyTradesMessage, MyTrades);
296data_request!(
297    GetEarnSummaryMessage,
298    GetEarnSummary,
299    EarnSummaryData,
300    EarnSummary
301);
302data_request!(GetMmSummaryMessage, GetMmSummary, MmSummaryData, MmSummary);
303data_request!(
304    GetTokenMarketsInfoMessage,
305    GetTokenMarketsInfo,
306    TokenMarketsInfoData,
307    TokenMarketsInfo
308);
309
310#[cfg(test)]
311mod tests {
312    use std::sync::Arc;
313
314    use super::*;
315    use crate::ws::managed::ManagedWsConfig;
316    use tokio::sync::mpsc::error::TryRecvError;
317
318    struct LeavesReadyWhileEncoding {
319        handle: ManagedWsHandle,
320        request_id: uuid::Uuid,
321    }
322
323    impl private::Sealed for LeavesReadyWhileEncoding {}
324
325    impl MakerDataRequest for LeavesReadyWhileEncoding {
326        type Response = MmSummaryData;
327
328        const RESPONSE_TYPE: &'static str = "MmSummary";
329
330        fn into_message(self) -> ClientMessage {
331            self.handle.inject_state(ManagedWsState::Authenticated {
332                connection_epoch: 7,
333            });
334            ClientMessage::GetMmSummary(GetMmSummaryMessage {
335                request_id: self.request_id,
336            })
337        }
338
339        fn into_response(message: ServerMessage) -> Result<Self::Response, Box<ServerMessage>> {
340            match message {
341                ServerMessage::MmSummary(response) => Ok(response),
342                other => Err(Box::new(other)),
343            }
344        }
345    }
346
347    fn config() -> ManagedWsConfig {
348        ManagedWsConfig::new(
349            "ws://localhost",
350            HelloData {
351                protocol_version: "1.0.0".to_string(),
352                features: Vec::new(),
353                client_name: None,
354                client_version: None,
355            },
356            Arc::new(crate::orders::BytesSigner::from_secret([1u8; 32])),
357            "owner",
358        )
359    }
360
361    #[test]
362    fn data_client_rejects_quote_endpoint_before_spawning() {
363        assert!(matches!(
364            MakerDataClient::spawn(config()),
365            Err(MakerClientError::WrongEndpoint {
366                required: MakerWsEndpoint::Data,
367                actual: MakerWsEndpoint::Quote,
368                ..
369            })
370        ));
371    }
372
373    #[test]
374    fn data_request_maps_to_one_typed_response() {
375        let request_id = uuid::Uuid::new_v4();
376        let request = GetActiveRfqsMessage { request_id };
377        assert!(matches!(
378            request.into_message(),
379            ClientMessage::GetActiveRfqs(GetActiveRfqsMessage {
380                request_id: actual
381            }) if actual == request_id
382        ));
383
384        let response = ServerMessage::ActiveRfqs(ActiveRfqsData {
385            request_id,
386            rfqs: Vec::new(),
387        });
388        assert_eq!(
389            <GetActiveRfqsMessage as MakerDataRequest>::into_response(response)
390                .expect("typed response")
391                .request_id,
392            request_id
393        );
394        assert!(matches!(
395            <GetActiveRfqsMessage as MakerDataRequest>::into_response(ServerMessage::RequireInvite),
396            Err(boxed) if matches!(*boxed, ServerMessage::RequireInvite)
397        ));
398    }
399
400    #[tokio::test]
401    async fn typed_request_does_not_enqueue_an_unbound_query_after_leaving_ready() {
402        let (handle, mut commands) = ManagedWsHandle::test_handle(1, 1);
403        handle.inject_state(ManagedWsState::Ready {
404            connection_epoch: 7,
405        });
406        let client = MakerDataClient {
407            inner: handle.clone(),
408        };
409
410        let result = tokio::select! {
411            result = client.request(
412                LeavesReadyWhileEncoding {
413                    handle,
414                    request_id: uuid::Uuid::new_v4(),
415                },
416                Duration::from_secs(1),
417            ) => result,
418            _ = commands.recv() => panic!("typed data request must not enqueue after leaving Ready"),
419        };
420
421        assert!(matches!(
422            result,
423            Err(MakerDataRequestError::Transport(SendAwaitError::NotReady))
424        ));
425        assert!(matches!(commands.try_recv(), Err(TryRecvError::Empty)));
426    }
427    #[tokio::test]
428    async fn data_client_exposes_all_owner_pushes() {
429        let (handle, _commands) = ManagedWsHandle::test_handle(1, 8);
430        let client = MakerDataClient {
431            inner: handle.clone(),
432        };
433        let mut messages = client.subscribe_messages();
434        for message in owner_pushes() {
435            let expected = serde_json::to_value(&message).unwrap();
436            handle.inject_message(message);
437            let received = messages.recv().await.unwrap();
438            assert_eq!(serde_json::to_value(received.message()).unwrap(), expected);
439        }
440    }
441
442    fn owner_pushes() -> Vec<ServerMessage> {
443        let caps = serde_json::json!({"positions": {"current": 1, "limit": 10}, "notional": [], "balances": []});
444        [
445        serde_json::json!({"type": "MakerCapsUpdated", "data": {"caps_snapshot": caps, "reason": "lifecycle"}}),
446        serde_json::json!({"type": "PositionUpdated", "data": {
447            "position": {"pda": "p", "market": "m", "underlying_mint": "u", "quote_mint": "q",
448                "position_type": "covered_call", "status": "open", "strike": 1, "quantity": 2,
449                "price": 3, "created_at": 1, "expiry_ts": 2},
450            "update_type": "created", "caps_snapshot": caps
451        }}),
452        serde_json::json!({"type": "TradeExecuted", "data": {"trade": {
453            "id": "00000000-0000-0000-0000-000000000001", "market": "m", "position_type": "covered_call",
454            "strike": 1, "quantity": 2, "price": 3, "taker": "t", "maker": "o", "tx_signature": "s", "executed_at": 1
455        }}}),
456    ].into_iter().map(|value| serde_json::from_value(value).unwrap()).collect()
457    }
458}