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 into_response(message: ServerMessage) -> Result<Self::Response, Box<ServerMessage>>;
30}
31
32pub trait PagedRequest: MakerDataRequest + Clone {
34 fn first_page(&mut self);
35 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 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
104impl 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#[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 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 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}