defi_tracker_lifecycle/lifecycle/
adapters.rs1use 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#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum CorrelationOutcome {
13 NotRequired,
16 Correlated(Vec<String>),
18 Uncorrelated { reason: String },
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum EventPayload {
26 None,
28 DcaFill { in_amount: i64, out_amount: i64 },
30 DcaClosed { status: TerminalStatus },
32 LimitFill {
34 in_amount: i64,
35 out_amount: i64,
36 remaining_in_amount: i64,
37 counterparty: String,
38 },
39 KaminoDisplay {
41 remaining_input_amount: i64,
42 filled_output_amount: i64,
43 terminal_status: Option<TerminalStatus>,
44 },
45}
46
47pub trait ProtocolAdapter: Sync {
49 fn protocol(&self) -> Protocol;
51
52 fn classify_instruction(&self, ix: &RawInstruction) -> Option<EventType>;
54
55 fn classify_and_resolve_event(
59 &self,
60 ev: &RawEvent,
61 ctx: &ResolveContext,
62 ) -> Option<Result<(EventType, CorrelationOutcome, EventPayload), Error>>;
63}
64
65pub 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
78pub 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
91pub 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}