Skip to main content

defi_tracker_lifecycle/lifecycle/
adapters.rs

1use crate::error::Error;
2use crate::lifecycle::TerminalStatus;
3use crate::protocols::dca::DcaAdapter;
4use crate::protocols::kamino::KaminoAdapter;
5use crate::protocols::limit_v1::LimitV1Adapter;
6use crate::protocols::limit_v2::LimitV2Adapter;
7use crate::protocols::{self, EventType, Protocol};
8use crate::types::{RawEvent, RawInstruction, ResolveContext};
9
10/// Whether (and how) an event was correlated to an order PDA.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum CorrelationOutcome {
13    /// Correlation is not meaningful for this event type
14    /// (e.g. Kamino `UserSwapBalancesEvent` is diagnostic-only).
15    NotRequired,
16    /// Event was successfully matched to one or more order PDAs.
17    Correlated(Vec<String>),
18    /// Event is the kind that *should* correlate, but context was missing
19    /// (e.g. Kamino `OrderDisplayEvent` without pre-fetched PDAs).
20    Uncorrelated { reason: String },
21}
22
23/// Protocol-specific data extracted from a resolved event.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum EventPayload {
26    /// No extra payload beyond the event type itself.
27    None,
28    /// Jupiter DCA fill amounts.
29    DcaFill { in_amount: i64, out_amount: i64 },
30    /// Jupiter DCA closed event with derived terminal status.
31    DcaClosed { status: TerminalStatus },
32    /// Jupiter Limit Order fill amounts (shared by V1 and V2).
33    LimitFill {
34        in_amount: i64,
35        out_amount: i64,
36        remaining_in_amount: i64,
37        counterparty: String,
38    },
39    /// Kamino order display snapshot with optional terminal status.
40    KaminoDisplay {
41        remaining_input_amount: i64,
42        filled_output_amount: i64,
43        terminal_status: Option<TerminalStatus>,
44    },
45}
46
47/// Stateless adapter for classifying instructions and resolving events for a single protocol.
48pub trait ProtocolAdapter: Sync {
49    /// Which protocol this adapter handles.
50    fn protocol(&self) -> Protocol;
51
52    /// Classifies a raw instruction into an [`EventType`], or `None` if unrecognised/irrelevant.
53    fn classify_instruction(&self, ix: &RawInstruction) -> Option<EventType>;
54
55    /// Classifies and resolves a raw event into an `(EventType, CorrelationOutcome, EventPayload)`.
56    ///
57    /// Returns `None` when `fields` is absent or the event name is unknown to this protocol.
58    fn classify_and_resolve_event(
59        &self,
60        ev: &RawEvent,
61        ctx: &ResolveContext,
62    ) -> Option<Result<(EventType, CorrelationOutcome, EventPayload), Error>>;
63}
64
65/// Derives a [`TerminalStatus`] from a DCA `ClosedEvent` payload.
66///
67/// Priority: `user_closed` → Cancelled, `unfilled_amount == 0` → Completed, else → Expired.
68pub fn dca_closed_terminal_status(closed: &protocols::dca::DcaClosedEvent) -> TerminalStatus {
69    if closed.user_closed {
70        TerminalStatus::Cancelled
71    } else if closed.unfilled_amount == 0 {
72        TerminalStatus::Completed
73    } else {
74        TerminalStatus::Expired
75    }
76}
77
78/// Converts a Kamino display status code into an optional [`TerminalStatus`].
79///
80/// Code 0 (Open) → `None`; codes 1–3 map to Completed/Cancelled/Expired.
81pub fn kamino_display_terminal_status(status_code: i64) -> Result<Option<TerminalStatus>, Error> {
82    let status = protocols::kamino::KaminoAdapter::parse_display_status(status_code)?;
83    match status {
84        protocols::kamino::KaminoDisplayStatus::Open => Ok(None),
85        protocols::kamino::KaminoDisplayStatus::Filled => Ok(Some(TerminalStatus::Completed)),
86        protocols::kamino::KaminoDisplayStatus::Cancelled => Ok(Some(TerminalStatus::Cancelled)),
87        protocols::kamino::KaminoDisplayStatus::Expired => Ok(Some(TerminalStatus::Expired)),
88    }
89}
90
91/// Returns the static [`ProtocolAdapter`] for the given protocol.
92pub fn adapter_for(protocol: Protocol) -> &'static dyn ProtocolAdapter {
93    match protocol {
94        Protocol::Dca => &DcaAdapter,
95        Protocol::LimitV1 => &LimitV1Adapter,
96        Protocol::LimitV2 => &LimitV2Adapter,
97        Protocol::Kamino => &KaminoAdapter,
98    }
99}
100
101#[cfg(test)]
102#[expect(clippy::unwrap_used, reason = "test assertions")]
103mod tests {
104    use super::*;
105    use crate::protocols::{EventType, Protocol};
106    use crate::types::{RawEvent, RawInstruction, ResolveContext};
107
108    fn make_instruction(name: &str) -> RawInstruction {
109        RawInstruction {
110            id: 1,
111            signature: "sig".to_string(),
112            instruction_index: 0,
113            instruction_path: None,
114            program_id: "p".to_string(),
115            inner_program_id: "p".to_string(),
116            instruction_name: name.to_string(),
117            accounts: None,
118            args: None,
119            slot: 1,
120        }
121    }
122
123    fn make_event(event_name: &str, fields: Option<serde_json::Value>) -> RawEvent {
124        RawEvent {
125            id: 1,
126            signature: "sig".to_string(),
127            event_index: 0,
128            event_path: None,
129            program_id: "p".to_string(),
130            inner_program_id: "p".to_string(),
131            event_name: event_name.to_string(),
132            fields,
133            slot: 1,
134        }
135    }
136
137    #[test]
138    fn adapter_selection_matches_protocol() {
139        assert_eq!(adapter_for(Protocol::Dca).protocol(), Protocol::Dca);
140        assert_eq!(adapter_for(Protocol::LimitV1).protocol(), Protocol::LimitV1);
141        assert_eq!(adapter_for(Protocol::LimitV2).protocol(), Protocol::LimitV2);
142        assert_eq!(adapter_for(Protocol::Kamino).protocol(), Protocol::Kamino);
143    }
144
145    #[test]
146    fn instruction_classifiers_map_known_names() {
147        let dca = adapter_for(Protocol::Dca);
148        assert_eq!(
149            dca.classify_instruction(&make_instruction("OpenDca")),
150            Some(EventType::Created)
151        );
152
153        let limit_v1 = adapter_for(Protocol::LimitV1);
154        assert_eq!(
155            limit_v1.classify_instruction(&make_instruction("FillOrder")),
156            Some(EventType::FillCompleted)
157        );
158
159        let limit_v2 = adapter_for(Protocol::LimitV2);
160        assert_eq!(
161            limit_v2.classify_instruction(&make_instruction("PreFlashFillOrder")),
162            Some(EventType::FillInitiated)
163        );
164
165        let kamino = adapter_for(Protocol::Kamino);
166        assert_eq!(
167            kamino.classify_instruction(&make_instruction("CreateOrder")),
168            Some(EventType::Created)
169        );
170    }
171
172    #[test]
173    fn dca_closed_terminal_status_user_cancelled() {
174        let closed = protocols::dca::DcaClosedEvent {
175            order_pda: "pda".to_string(),
176            user_closed: true,
177            unfilled_amount: 500,
178        };
179        assert_eq!(
180            dca_closed_terminal_status(&closed),
181            TerminalStatus::Cancelled
182        );
183    }
184
185    #[test]
186    fn dca_closed_terminal_status_completed() {
187        let closed = protocols::dca::DcaClosedEvent {
188            order_pda: "pda".to_string(),
189            user_closed: false,
190            unfilled_amount: 0,
191        };
192        assert_eq!(
193            dca_closed_terminal_status(&closed),
194            TerminalStatus::Completed
195        );
196    }
197
198    #[test]
199    fn dca_closed_terminal_status_expired() {
200        let closed = protocols::dca::DcaClosedEvent {
201            order_pda: "pda".to_string(),
202            user_closed: false,
203            unfilled_amount: 1000,
204        };
205        assert_eq!(dca_closed_terminal_status(&closed), TerminalStatus::Expired);
206    }
207
208    #[test]
209    fn kamino_display_terminal_status_all_codes() {
210        assert_eq!(kamino_display_terminal_status(0).unwrap(), None);
211        assert_eq!(
212            kamino_display_terminal_status(1).unwrap(),
213            Some(TerminalStatus::Completed)
214        );
215        assert_eq!(
216            kamino_display_terminal_status(2).unwrap(),
217            Some(TerminalStatus::Cancelled)
218        );
219        assert_eq!(
220            kamino_display_terminal_status(3).unwrap(),
221            Some(TerminalStatus::Expired)
222        );
223    }
224
225    #[test]
226    fn kamino_resolve_uncorrelated_without_context() {
227        let adapter = adapter_for(Protocol::Kamino);
228        let ev = RawEvent {
229            signature: "test_sig".to_string(),
230            ..make_event(
231                "OrderDisplayEvent",
232                Some(serde_json::json!({
233                    "OrderDisplayEvent": {
234                        "remaining_input_amount": 0,
235                        "filled_output_amount": 100,
236                        "number_of_fills": 1,
237                        "status": 1
238                    }
239                })),
240            )
241        };
242        let ctx = ResolveContext {
243            pre_fetched_order_pdas: None,
244        };
245
246        let result = adapter
247            .classify_and_resolve_event(&ev, &ctx)
248            .unwrap()
249            .unwrap();
250        let (_event_type, correlation, payload) = result;
251
252        assert!(matches!(
253            correlation,
254            CorrelationOutcome::Uncorrelated { .. }
255        ));
256        assert_eq!(payload, EventPayload::None);
257    }
258
259    #[test]
260    fn dca_adapter_resolves_opened_event() {
261        let adapter = adapter_for(Protocol::Dca);
262        let ev = make_event(
263            "OpenedEvent",
264            Some(serde_json::json!({
265                "OpenedEvent": { "dca_key": "dca_pda" }
266            })),
267        );
268
269        let (event_type, correlation, payload) = adapter
270            .classify_and_resolve_event(
271                &ev,
272                &ResolveContext {
273                    pre_fetched_order_pdas: None,
274                },
275            )
276            .unwrap()
277            .unwrap();
278
279        assert_eq!(event_type, EventType::Created);
280        assert_eq!(
281            correlation,
282            CorrelationOutcome::Correlated(vec!["dca_pda".to_string()])
283        );
284        assert_eq!(payload, EventPayload::None);
285    }
286
287    #[test]
288    fn limit_adapters_resolve_create_events() {
289        let limit_v1 = adapter_for(Protocol::LimitV1);
290        let limit_v1_event = make_event(
291            "CreateOrderEvent",
292            Some(serde_json::json!({
293                "CreateOrderEvent": { "order_key": "v1_order" }
294            })),
295        );
296        let (event_type_v1, _, _) = limit_v1
297            .classify_and_resolve_event(
298                &limit_v1_event,
299                &ResolveContext {
300                    pre_fetched_order_pdas: None,
301                },
302            )
303            .unwrap()
304            .unwrap();
305        assert_eq!(event_type_v1, EventType::Created);
306
307        let limit_v2 = adapter_for(Protocol::LimitV2);
308        let limit_v2_event = make_event(
309            "CreateOrderEvent",
310            Some(serde_json::json!({
311                "CreateOrderEvent": { "order_key": "v2_order" }
312            })),
313        );
314        let (event_type_v2, _, _) = limit_v2
315            .classify_and_resolve_event(
316                &limit_v2_event,
317                &ResolveContext {
318                    pre_fetched_order_pdas: None,
319                },
320            )
321            .unwrap()
322            .unwrap();
323        assert_eq!(event_type_v2, EventType::Created);
324    }
325
326    #[test]
327    fn classify_and_resolve_event_returns_none_when_fields_are_absent() {
328        let ev = make_event("AnyEvent", None);
329        let ctx = ResolveContext {
330            pre_fetched_order_pdas: None,
331        };
332
333        assert!(
334            adapter_for(Protocol::Dca)
335                .classify_and_resolve_event(&ev, &ctx)
336                .is_none()
337        );
338        assert!(
339            adapter_for(Protocol::LimitV1)
340                .classify_and_resolve_event(&ev, &ctx)
341                .is_none()
342        );
343        assert!(
344            adapter_for(Protocol::LimitV2)
345                .classify_and_resolve_event(&ev, &ctx)
346                .is_none()
347        );
348        assert!(
349            adapter_for(Protocol::Kamino)
350                .classify_and_resolve_event(&ev, &ctx)
351                .is_none()
352        );
353    }
354}