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 = 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/// Maker data-plane client.
245#[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    /// Fetch the complete response. A list request follows every page, so use
270    /// `request_page` to stop early.
271    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    /// Fetch exactly one page. Use `request_all` for a complete collection.
280    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    /// Every page of `request` at the server's largest page size; its own
309    /// `limit` and cursor are ignored. Pages after the first are appended.
310    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    /// Receive owner-direct `PositionUpdated`, `TradeExecuted` and
333    /// `MakerCapsUpdated` pushes alongside query responses. No subscription is
334    /// required. Owner pushes begin after `MmSummary` recovery; quote lifecycle
335    /// messages stay on the quote session. On reconnect or a stream gap, rebuild
336    /// authoritative state (for example with `maker_open_positions`).
337    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}