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 = crate::paging::MAX_PAGE_SIZE;
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 = crate::paging::MAX_PAGE_SIZE;
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.indicatives.iter_mut().find(|i| {
169 i.position_type == indicative.position_type && i.side == indicative.side
170 }) {
171 current.strikes.extend(indicative.strikes);
172 } else {
173 existing.indicatives.push(indicative);
174 }
175 }
176 } else {
177 all.markets.push(market);
178 }
179 }
180 all.next_cursor = page.next_cursor;
181 }
182}
183
184pub struct MakerResponse<T> {
185 payload: T,
186}
187
188impl<T> MakerResponse<T> {
189 #[must_use]
190 pub const fn payload(&self) -> &T {
191 &self.payload
192 }
193
194 #[must_use]
195 pub fn into_payload(self) -> T {
196 self.payload
197 }
198}
199
200impl<T> Deref for MakerResponse<T> {
201 type Target = T;
202
203 fn deref(&self) -> &Self::Target {
204 &self.payload
205 }
206}
207
208#[derive(Debug, thiserror::Error)]
209#[non_exhaustive]
210pub enum MakerDataRequestError {
211 #[error("page claims more rows but provides no advancing cursor")]
212 InvalidPage,
213 #[error(transparent)]
214 Continuation(#[from] crate::paging::InvalidPage),
215 #[error(transparent)]
216 Session(#[from] ManagedWsError),
217 #[error(transparent)]
218 Transport(#[from] SendAwaitError),
219 #[error("server rejected request {request_id}")]
220 Rejected {
221 request_id: uuid::Uuid,
222 response: Arc<ServerMessage>,
223 },
224 #[error("expected {expected}, received a different correlated response")]
225 UnexpectedResponse {
226 expected: &'static str,
227 response: Arc<ServerMessage>,
228 },
229}
230
231impl MakerDataRequestError {
232 #[must_use]
233 pub fn server_error(&self) -> Option<&ServerError> {
234 match self {
235 Self::Rejected { response, .. } => match &**response {
236 ServerMessage::RequestError(error) => Some(&error.error),
237 _ => None,
238 },
239 _ => None,
240 }
241 }
242}
243
244#[derive(Clone)]
246pub struct MakerDataClient {
247 inner: ManagedWsHandle,
248}
249
250impl MakerDataClient {
251 pub fn spawn(config: ManagedWsConfig) -> Result<Self, MakerClientError> {
252 let config = Self::prepare_config(config)?;
253 Ok(Self {
254 inner: spawn_managed_ws(config)?,
255 })
256 }
257
258 fn prepare_config(config: ManagedWsConfig) -> Result<ManagedWsConfig, MakerClientError> {
259 if config.endpoint != MakerWsEndpoint::Data {
260 return Err(MakerClientError::WrongEndpoint {
261 client: "MakerDataClient",
262 required: MakerWsEndpoint::Data,
263 actual: config.endpoint,
264 });
265 }
266 Ok(config)
267 }
268
269 pub async fn request_all<R: MakerDataRequest>(
272 &self,
273 request: R,
274 timeout: Duration,
275 ) -> Result<MakerResponse<R::Response>, MakerDataRequestError> {
276 request.fetch(self, timeout).await
277 }
278
279 pub async fn request_page<R: MakerDataRequest>(
281 &self,
282 request: R,
283 timeout: Duration,
284 ) -> Result<MakerResponse<R::Response>, MakerDataRequestError> {
285 let message = self
286 .inner
287 .send_await_ready(request.into_message(), timeout)
288 .await?;
289 if let ServerMessage::RequestError(error) = &*message {
290 return Err(MakerDataRequestError::Rejected {
291 request_id: error.request_id,
292 response: message,
293 });
294 }
295 let message = match Arc::try_unwrap(message) {
296 Ok(owned) => owned,
297 Err(shared) => (*shared).clone(),
298 };
299 match R::into_response(message) {
300 Ok(payload) => Ok(MakerResponse { payload }),
301 Err(other) => Err(MakerDataRequestError::UnexpectedResponse {
302 expected: R::RESPONSE_TYPE,
303 response: Arc::from(other),
304 }),
305 }
306 }
307
308 pub async fn all_pages<R: PagedRequest>(
311 &self,
312 mut request: R,
313 timeout: Duration,
314 ) -> Result<R::Response, MakerDataRequestError> {
315 request.first_page();
316 crate::paging::fetch_all(
317 |cursor| {
318 let mut next = request.clone();
319 next.request_cursor(cursor);
320 async move {
321 self.request_page(next, timeout)
322 .await
323 .map(MakerResponse::into_payload)
324 }
325 },
326 R::cursor,
327 R::append,
328 )
329 .await
330 }
331
332 pub fn subscribe_messages(&self) -> ManagedMessageReceiver {
338 self.inner.subscribe_messages()
339 }
340
341 pub fn subscribe_reference_changes(&self) -> ReferenceChangeReceiver {
342 self.inner.subscribe_reference_changes()
343 }
344
345 pub fn subscribe_events(&self) -> broadcast::Receiver<ManagedWsEvent> {
346 self.inner.subscribe_events()
347 }
348
349 #[must_use]
350 pub fn state(&self) -> ManagedWsState {
351 self.inner.state()
352 }
353
354 pub async fn wait_until_ready(&self) -> Result<u64, WaitUntilReadyError> {
355 self.inner.wait_until_ready().await
356 }
357
358 #[must_use]
359 pub const fn raw(&self) -> &ManagedWsHandle {
360 &self.inner
361 }
362
363 #[must_use]
364 pub fn into_raw(self) -> ManagedWsHandle {
365 self.inner
366 }
367
368 pub async fn close(&self) -> Result<(), ManagedWsError> {
369 self.inner.close().await
370 }
371}
372
373macro_rules! paged_fetch {
374 (paged) => {
375 async fn fetch(
376 self,
377 client: &MakerDataClient,
378 timeout: Duration,
379 ) -> Result<MakerResponse<Self::Response>, MakerDataRequestError> {
380 client
381 .all_pages(self, timeout)
382 .await
383 .map(|payload| MakerResponse { payload })
384 }
385 };
386}
387
388macro_rules! data_request {
389 ($request:ty, $client_variant:ident, $response:ty, $server_variant:ident $(, $paged:ident)?) => {
390 impl private::Sealed for $request {}
391
392 impl MakerDataRequest for $request {
393 type Response = $response;
394
395 $(paged_fetch!($paged);)?
396
397 const RESPONSE_TYPE: &'static str = stringify!($server_variant);
398
399 fn into_message(self) -> ClientMessage {
400 ClientMessage::$client_variant(self)
401 }
402
403 fn into_response(message: ServerMessage) -> Result<Self::Response, Box<ServerMessage>> {
404 match message {
405 ServerMessage::$server_variant(response) => Ok(response),
406 other => Err(Box::new(other)),
407 }
408 }
409 }
410 };
411}
412
413data_request!(
414 GetActiveRfqsMessage,
415 GetActiveRfqs,
416 ActiveRfqsData,
417 ActiveRfqs,
418 paged
419);
420data_request!(GetMarketsMessage, GetMarkets, MarketsData, Markets, paged);
421data_request!(
422 GetMarketDescriptorsMessage,
423 GetMarketDescriptors,
424 MarketDescriptorsData,
425 MarketDescriptors,
426 paged
427);
428data_request!(
429 GetExpiriesMessage,
430 GetExpiries,
431 ExpiriesData,
432 Expiries,
433 paged
434);
435data_request!(GetTokensMessage, GetTokens, TokensData, Tokens, paged);
436data_request!(
437 GetIndicativePricesMessage,
438 GetIndicativePrices,
439 IndicativePricesMessage,
440 IndicativePrices,
441 paged
442);
443data_request!(
444 GetMakerPositionsMessage,
445 GetMakerPositions,
446 MakerPositionsMessage,
447 MakerPositions,
448 paged
449);
450data_request!(
451 GetMyQuotesMessage,
452 GetMyQuotes,
453 MyQuotesMessage,
454 MyQuotes,
455 paged
456);
457data_request!(
458 GetOrderStatusMessage,
459 GetOrderStatus,
460 OrderStatusMessage,
461 OrderStatus
462);
463data_request!(
464 GetMarketsForMakerMessage,
465 GetMarketsForMaker,
466 MakerMarketsMessage,
467 MakerMarkets,
468 paged
469);
470data_request!(
471 GetTokenCapsMessage,
472 GetTokenCaps,
473 TokenCapsData,
474 TokenCaps,
475 paged
476);
477data_request!(GetMyCapsMessage, GetMyCaps, MyCapsData, MyCaps, paged);
478data_request!(CheckQuoteMessage, CheckQuote, QuoteCheckData, QuoteCheck);
479data_request!(
480 GetMyTradesMessage,
481 GetMyTrades,
482 MyTradesMessage,
483 MyTrades,
484 paged
485);
486data_request!(
487 GetEarnSummaryMessage,
488 GetEarnSummary,
489 EarnSummaryData,
490 EarnSummary,
491 paged
492);
493data_request!(
494 GetMmSummaryMessage,
495 GetMmSummary,
496 MmSummaryData,
497 MmSummary,
498 paged
499);
500data_request!(
501 GetTokenMarketsInfoMessage,
502 GetTokenMarketsInfo,
503 TokenMarketsInfoData,
504 TokenMarketsInfo,
505 paged
506);
507
508data_request!(
509 GetPositionsMessage,
510 GetPositions,
511 PositionsData,
512 Positions,
513 paged
514);
515data_request!(
516 GetPositionSummaryMessage,
517 GetPositionSummary,
518 PositionSummaryData,
519 PositionSummary,
520 paged
521);
522data_request!(
523 GetMyActiveRfqsMessage,
524 GetMyActiveRfqs,
525 MyActiveRfqsData,
526 MyActiveRfqs,
527 paged
528);
529data_request!(
530 GetSubscriptionsMessage,
531 GetSubscriptions,
532 SubscriptionsMessage,
533 Subscriptions,
534 paged
535);
536
537#[cfg(test)]
538mod tests {
539 use std::sync::Arc;
540
541 use super::*;
542 use crate::ws::managed::ManagedWsConfig;
543 use tokio::sync::mpsc::error::TryRecvError;
544
545 struct LeavesReadyWhileEncoding {
546 handle: ManagedWsHandle,
547 request_id: uuid::Uuid,
548 }
549
550 impl private::Sealed for LeavesReadyWhileEncoding {}
551
552 impl MakerDataRequest for LeavesReadyWhileEncoding {
553 type Response = MmSummaryData;
554
555 const RESPONSE_TYPE: &'static str = "MmSummary";
556
557 fn into_message(self) -> ClientMessage {
558 self.handle.inject_state(ManagedWsState::Authenticated {
559 connection_epoch: 7,
560 });
561 ClientMessage::GetMmSummary(GetMmSummaryMessage {
562 limit: crate::paging::MAX_PAGE_SIZE,
563 cursor: crate::ws::types::Cursor::Start {},
564
565 request_id: self.request_id,
566 })
567 }
568
569 fn into_response(message: ServerMessage) -> Result<Self::Response, Box<ServerMessage>> {
570 match message {
571 ServerMessage::MmSummary(response) => Ok(response),
572 other => Err(Box::new(other)),
573 }
574 }
575 }
576
577 fn config() -> ManagedWsConfig {
578 ManagedWsConfig::new(
579 "ws://localhost",
580 HelloData {
581 protocol_version: crate::WS_PROTOCOL_VERSION.to_string(),
582 on_disconnect: crate::ws::types::OnDisconnect::KeepQuotes,
583 client_name: "test".to_string(),
584 client_version: "test".to_string(),
585 },
586 Arc::new(crate::orders::BytesSigner::from_secret([1u8; 32])),
587 crate::Address32::new([76; 32]),
588 )
589 }
590
591 #[test]
592 fn data_client_rejects_quote_endpoint_before_spawning() {
593 assert!(matches!(
594 MakerDataClient::spawn(config()),
595 Err(MakerClientError::WrongEndpoint {
596 required: MakerWsEndpoint::Data,
597 actual: MakerWsEndpoint::Quote,
598 ..
599 })
600 ));
601 }
602
603 #[test]
604 fn continuations_use_the_server_key_even_without_renderable_rows() {
605 let mut request = GetMyQuotesMessage::default();
606 request.first_page();
607 let mut all = MyQuotesMessage {
608 request_id: request.request_id,
609 quotes: Vec::new(),
610 next_cursor: Some("opaque".into()),
611 };
612 assert!(request.next_page(&all));
613 assert_eq!(
614 request.cursor,
615 Cursor::After {
616 id: "opaque".into()
617 }
618 );
619 assert!(!request.next_page(&all));
620 GetMyQuotesMessage::append(
621 &mut all,
622 MyQuotesMessage {
623 request_id: request.request_id,
624 quotes: Vec::new(),
625 next_cursor: None,
626 },
627 );
628 assert!(!GetMyQuotesMessage::has_more(&all));
629 }
630
631 #[test]
632 fn token_market_pages_merge_per_product_and_side() {
633 let page = |side: &str, strike: u64, next: Option<&str>| -> TokenMarketsInfoData {
634 serde_json::from_value(serde_json::json!({
635 "next_cursor": next, "request_id": "00000000-0000-0000-0000-000000000000",
636 "underlying_symbol": "SOL", "underlying_decimals": 9, "quote_symbol": "USDC", "quote_decimals": 6,
637 "size_rule": { "min_size": 1, "max_size": 10, "step": 1 }, "reference_price": 1,
638 "markets": [{ "products": ["covered_call"], "market_pda": "11111111111111111111111111111111",
639 "expiry_ts": 10, "is_put": false,
640 "indicatives": [{ "position_type": "covered_call", "side": side, "updated_at": 0, "is_stale": false,
641 "strikes": [{ "strike": strike, "best_price": strike + 100 }] }] }]
642 }))
643 .unwrap()
644 };
645 let mut all = page("sell", 1, Some("a"));
646 for next in [
647 page("sell", 2, Some("b")),
648 page("buy", 1, Some("c")),
649 page("buy", 2, None),
650 ] {
651 GetTokenMarketsInfoMessage::append(&mut all, next);
652 }
653 let sides: Vec<_> = all.markets[0]
654 .indicatives
655 .iter()
656 .map(|i| {
657 (
658 i.side,
659 i.strikes
660 .iter()
661 .map(|s| s.strike.value())
662 .collect::<Vec<_>>(),
663 )
664 })
665 .collect();
666 assert_eq!(
667 sides,
668 vec![
669 (crate::types::TakerSide::Sell, vec![1, 2]),
670 (crate::types::TakerSide::Buy, vec![1, 2])
671 ]
672 );
673 }
674
675 #[test]
676 fn empty_continuation_is_rejected() {
677 let mut request = GetMyQuotesMessage::default();
678 let page = MyQuotesMessage {
679 request_id: request.request_id,
680 quotes: Vec::new(),
681 next_cursor: Some(String::new()),
682 };
683 assert!(!request.next_page(&page));
684 }
685
686 #[test]
687 fn data_request_maps_to_one_typed_response() {
688 let request_id = uuid::Uuid::new_v4();
689 let request = GetActiveRfqsMessage {
690 limit: crate::paging::MAX_PAGE_SIZE,
691 cursor: crate::ws::types::Cursor::Start {},
692 request_id,
693 };
694 assert!(matches!(
695 request.into_message(),
696 ClientMessage::GetActiveRfqs(GetActiveRfqsMessage {
697 limit: crate::paging::MAX_PAGE_SIZE,
698 cursor: crate::ws::types::Cursor::Start {},
699
700 request_id: actual
701 }) if actual == request_id
702 ));
703
704 let response = ServerMessage::ActiveRfqs(ActiveRfqsData {
705 next_cursor: None,
706
707 request_id,
708 rfqs: Vec::new(),
709 });
710 assert_eq!(
711 <GetActiveRfqsMessage as MakerDataRequest>::into_response(response)
712 .expect("typed response")
713 .request_id,
714 request_id
715 );
716 assert!(matches!(
717 <GetActiveRfqsMessage as MakerDataRequest>::into_response(ServerMessage::RequireInvite),
718 Err(boxed) if matches!(*boxed, ServerMessage::RequireInvite)
719 ));
720 }
721
722 #[tokio::test]
723 async fn typed_request_does_not_enqueue_an_unbound_query_after_leaving_ready() {
724 let (handle, mut commands) = ManagedWsHandle::test_handle(1, 1);
725 handle.inject_state(ManagedWsState::Ready {
726 connection_epoch: 7,
727 });
728 let client = MakerDataClient {
729 inner: handle.clone(),
730 };
731
732 let result = tokio::select! {
733 result = client.request_all(
734 LeavesReadyWhileEncoding {
735 handle,
736 request_id: uuid::Uuid::new_v4(),
737 },
738 Duration::from_secs(1),
739 ) => result,
740 _ = commands.recv() => panic!("typed data request must not enqueue after leaving Ready"),
741 };
742
743 assert!(matches!(
744 result,
745 Err(MakerDataRequestError::Transport(SendAwaitError::NotReady))
746 ));
747 assert!(matches!(commands.try_recv(), Err(TryRecvError::Empty)));
748 }
749 #[tokio::test]
750 async fn data_client_exposes_all_owner_pushes() {
751 let (handle, _commands) = ManagedWsHandle::test_handle(1, 8);
752 let client = MakerDataClient {
753 inner: handle.clone(),
754 };
755 let mut messages = client.subscribe_messages();
756 for message in owner_pushes() {
757 let expected = serde_json::to_value(&message).unwrap();
758 handle.inject_message(message);
759 let received = messages.recv().await.unwrap();
760 assert_eq!(serde_json::to_value(received.message()).unwrap(), expected);
761 }
762 }
763
764 fn owner_pushes() -> Vec<ServerMessage> {
765 let caps = serde_json::json!({"positions": {"current": 1, "limit": 10}, "notional": [], "balances": [], "quote_liabilities": []});
766 [
767 serde_json::json!({"type": "MakerCapsUpdated", "data": {"caps_snapshot": caps, "reason": "lifecycle"}}),
768 serde_json::json!({"type": "PositionUpdated", "data": {
769 "position": {"revision": {"slot": 1, "transaction_index": 0, "instruction_index": 0}, "pda": "2PEfSGdYo17syw6vewKJceXbKHV481KWjXbo1x6g9E9D", "market": "11111111111111111111111111111111", "underlying_mint": "opdhLMLuFCTUEq3YCwdtgqfuhtMqdkHBsJf6YrHfUFF", "quote_mint": "Aa8SVbHR7yKKA6Zq2M2Yw9jGvNsZkMD6QKwCCCR7vgeA",
770 "terms": {"type": "covered_call", "strike": 1}, "side": "sell", "state": "open", "quantity": 2,
771 "price": 3, "total_premium": 6, "created_at": 1, "expiry_ts": 2,
772 "underlying_decimals": 9, "quote_decimals": 6,
773 "underlying_price": {"type": "available", "price": 150, "published_at": 1}},
774 "update_type": "created"
775 }}),
776 serde_json::json!({"type": "TradeExecuted", "data": {"trade": {
777 "id": "00000000-0000-0000-0000-000000000001", "market": "11111111111111111111111111111111", "terms": {"type": "covered_call", "strike": 1}, "side": "sell",
778 "quantity": 2, "price": 3, "taker": "GKwe8PT8YLdJiztcH7TpVkf7GVHFtHu4tNW9x5woDp6T", "maker": "7rJSLqoFCCqN39ztKiotFqaxwxEtycG4Fm8XzF4Bvx2M", "tx_signature": "s", "executed_at": 1
779 }}}),
780 ].into_iter().map(|value| serde_json::from_value(value).unwrap()).collect()
781 }
782}