1use crate::error::Error;
2use crate::lifecycle::adapters::{
3 CorrelationOutcome, EventPayload, ProtocolAdapter, dca_closed_terminal_status,
4};
5use crate::protocols::{AccountInfo, EventType, Protocol, ProtocolHelpers};
6use crate::types::{RawEvent, RawInstruction, ResolveContext};
7use strum::VariantNames;
8
9#[derive(serde::Deserialize, strum_macros::VariantNames)]
13pub enum DcaEventEnvelope {
14 OpenedEvent(DcaKeyHolder),
15 FilledEvent(FilledEventFields),
16 ClosedEvent(ClosedEventFields),
17 CollectedFeeEvent(DcaKeyHolder),
18 WithdrawEvent(DcaKeyHolder),
19 DepositEvent(DcaKeyHolder),
20}
21
22#[derive(serde::Deserialize)]
26pub enum DcaInstructionKind {
27 OpenDca(serde_json::Value),
28 OpenDcaV2(serde_json::Value),
29 InitiateFlashFill(serde_json::Value),
30 InitiateDlmmFill(serde_json::Value),
31 FulfillFlashFill(serde_json::Value),
32 FulfillDlmmFill(serde_json::Value),
33 CloseDca(serde_json::Value),
34 EndAndClose(serde_json::Value),
35 Transfer(serde_json::Value),
36 Deposit(serde_json::Value),
37 Withdraw(serde_json::Value),
38 WithdrawFees(serde_json::Value),
39}
40
41pub const INSTRUCTION_EVENT_TYPES: &[(&str, EventType)] = &[
42 ("OpenDca", EventType::Created),
43 ("OpenDcaV2", EventType::Created),
44 ("InitiateFlashFill", EventType::FillInitiated),
45 ("InitiateDlmmFill", EventType::FillInitiated),
46 ("FulfillFlashFill", EventType::FillCompleted),
47 ("FulfillDlmmFill", EventType::FillCompleted),
48 ("CloseDca", EventType::Closed),
49 ("EndAndClose", EventType::Closed),
50];
51
52pub const EVENT_EVENT_TYPES: &[(&str, EventType)] = &[
53 ("OpenedEvent", EventType::Created),
54 ("FilledEvent", EventType::FillCompleted),
55 ("ClosedEvent", EventType::Closed),
56 ("CollectedFeeEvent", EventType::FeeCollected),
57 ("WithdrawEvent", EventType::Withdrawn),
58 ("DepositEvent", EventType::Deposited),
59];
60
61pub const CLOSED_VARIANTS: &[&str] = &["Completed", "Cancelled", "Expired"];
62
63#[derive(Debug)]
65pub struct DcaAdapter;
66
67#[derive(serde::Deserialize)]
69pub struct FilledEventFields {
70 dca_key: String,
71 in_amount: u64,
72 out_amount: u64,
73}
74
75#[derive(serde::Deserialize)]
77pub struct ClosedEventFields {
78 dca_key: String,
79 user_closed: bool,
80 unfilled_amount: u64,
81}
82
83#[derive(serde::Deserialize)]
85pub struct DcaKeyHolder {
86 dca_key: String,
87}
88
89pub struct DcaClosedEvent {
91 pub order_pda: String,
92 pub user_closed: bool,
93 pub unfilled_amount: i64,
94}
95
96pub struct DcaFillEvent {
98 pub order_pda: String,
99 pub in_amount: i64,
100 pub out_amount: i64,
101}
102
103pub struct DcaCreateArgs {
105 pub in_amount: i64,
106 pub in_amount_per_cycle: i64,
107 pub cycle_frequency: i64,
108 pub min_out_amount: Option<i64>,
109 pub max_out_amount: Option<i64>,
110 pub start_at: Option<i64>,
111}
112
113pub struct DcaCreateMints {
115 pub input_mint: String,
116 pub output_mint: String,
117}
118
119#[derive(serde::Deserialize)]
120struct OpenDcaFields {
121 in_amount: u64,
122 in_amount_per_cycle: u64,
123 cycle_frequency: i64,
124 min_out_amount: Option<u64>,
125 max_out_amount: Option<u64>,
126 start_at: Option<i64>,
127}
128
129impl ProtocolAdapter for DcaAdapter {
130 fn protocol(&self) -> Protocol {
131 Protocol::Dca
132 }
133
134 fn classify_instruction(&self, ix: &RawInstruction) -> Option<EventType> {
135 ProtocolHelpers::lookup_event_type(&ix.instruction_name, INSTRUCTION_EVENT_TYPES)
136 }
137
138 fn classify_and_resolve_event(
139 &self,
140 ev: &RawEvent,
141 _ctx: &ResolveContext,
142 ) -> Option<Result<(EventType, CorrelationOutcome, EventPayload), Error>> {
143 let fields = ev.fields.as_ref()?;
144 let envelope: DcaEventEnvelope = match serde_json::from_value(fields.clone()) {
145 Ok(e) => e,
146 Err(err) => {
147 if !ProtocolHelpers::contains_known_variant(fields, DcaEventEnvelope::VARIANTS) {
148 return None;
149 }
150 return Some(Err(Error::Protocol {
151 reason: format!("failed to parse DCA event payload: {err}"),
152 }));
153 }
154 };
155
156 Some(Self::resolve_event(envelope))
157 }
158}
159
160impl DcaAdapter {
161 fn resolve_event(
162 envelope: DcaEventEnvelope,
163 ) -> Result<(EventType, CorrelationOutcome, EventPayload), Error> {
164 match envelope {
165 DcaEventEnvelope::FilledEvent(FilledEventFields {
166 dca_key,
167 in_amount,
168 out_amount,
169 }) => Ok((
170 EventType::FillCompleted,
171 CorrelationOutcome::Correlated(vec![dca_key]),
172 EventPayload::DcaFill {
173 in_amount: ProtocolHelpers::checked_u64_to_i64(in_amount, "in_amount")?,
174 out_amount: ProtocolHelpers::checked_u64_to_i64(out_amount, "out_amount")?,
175 },
176 )),
177 DcaEventEnvelope::ClosedEvent(ClosedEventFields {
178 dca_key,
179 user_closed,
180 unfilled_amount,
181 }) => {
182 let closed = DcaClosedEvent {
183 order_pda: dca_key,
184 user_closed,
185 unfilled_amount: ProtocolHelpers::checked_u64_to_i64(
186 unfilled_amount,
187 "unfilled_amount",
188 )?,
189 };
190 let status = dca_closed_terminal_status(&closed);
191 Ok((
192 EventType::Closed,
193 CorrelationOutcome::Correlated(vec![closed.order_pda]),
194 EventPayload::DcaClosed { status },
195 ))
196 }
197 DcaEventEnvelope::OpenedEvent(DcaKeyHolder { dca_key }) => Ok((
198 EventType::Created,
199 CorrelationOutcome::Correlated(vec![dca_key]),
200 EventPayload::None,
201 )),
202 DcaEventEnvelope::CollectedFeeEvent(DcaKeyHolder { dca_key }) => Ok((
203 EventType::FeeCollected,
204 CorrelationOutcome::Correlated(vec![dca_key]),
205 EventPayload::None,
206 )),
207 DcaEventEnvelope::WithdrawEvent(DcaKeyHolder { dca_key }) => Ok((
208 EventType::Withdrawn,
209 CorrelationOutcome::Correlated(vec![dca_key]),
210 EventPayload::None,
211 )),
212 DcaEventEnvelope::DepositEvent(DcaKeyHolder { dca_key }) => Ok((
213 EventType::Deposited,
214 CorrelationOutcome::Correlated(vec![dca_key]),
215 EventPayload::None,
216 )),
217 }
218 }
219
220 pub fn extract_order_pda(
224 accounts: &[AccountInfo],
225 instruction_name: &str,
226 ) -> Result<String, Error> {
227 if let Some(acc) = ProtocolHelpers::find_account_by_name(accounts, "dca") {
228 return Ok(acc.pubkey.clone());
229 }
230
231 let wrapper = serde_json::json!({ instruction_name: serde_json::Value::Null });
232 let kind: DcaInstructionKind =
233 serde_json::from_value(wrapper).map_err(|_| Error::Protocol {
234 reason: format!("unknown DCA instruction: {instruction_name}"),
235 })?;
236
237 let idx = match kind {
238 DcaInstructionKind::OpenDca(_) | DcaInstructionKind::OpenDcaV2(_) => 0,
239 DcaInstructionKind::InitiateFlashFill(_)
240 | DcaInstructionKind::FulfillFlashFill(_)
241 | DcaInstructionKind::InitiateDlmmFill(_)
242 | DcaInstructionKind::FulfillDlmmFill(_) => 1,
243 DcaInstructionKind::CloseDca(_) | DcaInstructionKind::EndAndClose(_) => 1,
244 DcaInstructionKind::Transfer(_)
245 | DcaInstructionKind::Deposit(_)
246 | DcaInstructionKind::Withdraw(_)
247 | DcaInstructionKind::WithdrawFees(_) => {
248 return Err(Error::Protocol {
249 reason: format!("DCA instruction {instruction_name} has no order PDA"),
250 });
251 }
252 };
253
254 accounts
255 .get(idx)
256 .map(|a| a.pubkey.clone())
257 .ok_or_else(|| Error::Protocol {
258 reason: format!("DCA account index {idx} out of bounds for {instruction_name}"),
259 })
260 }
261
262 pub fn extract_create_mints(
266 accounts: &[AccountInfo],
267 instruction_name: &str,
268 ) -> Result<DcaCreateMints, Error> {
269 let input_mint =
270 ProtocolHelpers::find_account_by_name(accounts, "input_mint").map(|a| a.pubkey.clone());
271 let output_mint = ProtocolHelpers::find_account_by_name(accounts, "output_mint")
272 .map(|a| a.pubkey.clone());
273
274 if let (Some(input_mint), Some(output_mint)) = (input_mint, output_mint) {
275 return Ok(DcaCreateMints {
276 input_mint,
277 output_mint,
278 });
279 }
280
281 let wrapper = serde_json::json!({ instruction_name: serde_json::Value::Null });
282 let kind: DcaInstructionKind =
283 serde_json::from_value(wrapper).map_err(|_| Error::Protocol {
284 reason: format!("unknown DCA instruction: {instruction_name}"),
285 })?;
286
287 let (input_idx, output_idx) = match kind {
288 DcaInstructionKind::OpenDca(_) => (2, 3),
289 DcaInstructionKind::OpenDcaV2(_) => (3, 4),
290 _ => {
291 return Err(Error::Protocol {
292 reason: format!("not a DCA create instruction: {instruction_name}"),
293 });
294 }
295 };
296
297 let input_mint = accounts
298 .get(input_idx)
299 .map(|a| a.pubkey.clone())
300 .ok_or_else(|| Error::Protocol {
301 reason: format!("DCA input_mint index {input_idx} out of bounds"),
302 })?;
303 let output_mint = accounts
304 .get(output_idx)
305 .map(|a| a.pubkey.clone())
306 .ok_or_else(|| Error::Protocol {
307 reason: format!("DCA output_mint index {output_idx} out of bounds"),
308 })?;
309
310 Ok(DcaCreateMints {
311 input_mint,
312 output_mint,
313 })
314 }
315
316 pub fn parse_create_args(args: &serde_json::Value) -> Result<DcaCreateArgs, Error> {
318 let OpenDcaFields {
319 in_amount,
320 in_amount_per_cycle,
321 cycle_frequency,
322 min_out_amount,
323 max_out_amount,
324 start_at,
325 } = serde_json::from_value(args.clone()).map_err(|e| Error::Protocol {
326 reason: format!("failed to parse DCA create args: {e}"),
327 })?;
328
329 Ok(DcaCreateArgs {
330 in_amount: ProtocolHelpers::checked_u64_to_i64(in_amount, "in_amount")?,
331 in_amount_per_cycle: ProtocolHelpers::checked_u64_to_i64(
332 in_amount_per_cycle,
333 "in_amount_per_cycle",
334 )?,
335 cycle_frequency,
336 min_out_amount: min_out_amount.and_then(ProtocolHelpers::optional_u64_to_i64),
337 max_out_amount: max_out_amount.and_then(ProtocolHelpers::optional_u64_to_i64),
338 start_at: start_at.filter(|&ts| ts > 0),
339 })
340 }
341
342 #[cfg(all(test, feature = "native"))]
343 pub fn classify_decoded(
344 decoded: &carbon_jupiter_dca_decoder::instructions::JupiterDcaInstruction,
345 ) -> Option<EventType> {
346 use carbon_jupiter_dca_decoder::instructions::JupiterDcaInstruction;
347 match decoded {
348 JupiterDcaInstruction::OpenDca(_) | JupiterDcaInstruction::OpenDcaV2(_) => {
349 Some(EventType::Created)
350 }
351 JupiterDcaInstruction::InitiateFlashFill(_)
352 | JupiterDcaInstruction::InitiateDlmmFill(_) => Some(EventType::FillInitiated),
353 JupiterDcaInstruction::FulfillFlashFill(_)
354 | JupiterDcaInstruction::FulfillDlmmFill(_) => Some(EventType::FillCompleted),
355 JupiterDcaInstruction::CloseDca(_) | JupiterDcaInstruction::EndAndClose(_) => {
356 Some(EventType::Closed)
357 }
358 JupiterDcaInstruction::OpenedEvent(_) => Some(EventType::Created),
359 JupiterDcaInstruction::FilledEvent(_) => Some(EventType::FillCompleted),
360 JupiterDcaInstruction::ClosedEvent(_) => Some(EventType::Closed),
361 JupiterDcaInstruction::CollectedFeeEvent(_) => Some(EventType::FeeCollected),
362 JupiterDcaInstruction::WithdrawEvent(_) => Some(EventType::Withdrawn),
363 JupiterDcaInstruction::DepositEvent(_) => Some(EventType::Deposited),
364 JupiterDcaInstruction::Transfer(_)
365 | JupiterDcaInstruction::Deposit(_)
366 | JupiterDcaInstruction::Withdraw(_)
367 | JupiterDcaInstruction::WithdrawFees(_) => None,
368 }
369 }
370}
371
372#[cfg(test)]
373#[expect(
374 clippy::unwrap_used,
375 clippy::expect_used,
376 clippy::panic,
377 reason = "test assertions"
378)]
379mod tests {
380 use super::*;
381 use crate::lifecycle::TerminalStatus;
382
383 fn account(pubkey: &str, name: Option<&str>) -> AccountInfo {
384 AccountInfo {
385 pubkey: pubkey.to_string(),
386 is_signer: false,
387 is_writable: false,
388 name: name.map(str::to_string),
389 }
390 }
391
392 fn make_event(fields: serde_json::Value) -> RawEvent {
393 RawEvent {
394 id: 1,
395 signature: "sig".to_string(),
396 event_index: 0,
397 event_path: None,
398 program_id: "p".to_string(),
399 inner_program_id: "p".to_string(),
400 event_name: "test".to_string(),
401 fields: Some(fields),
402 slot: 1,
403 }
404 }
405
406 fn resolve(
407 fields: serde_json::Value,
408 ) -> Option<Result<(EventType, CorrelationOutcome, EventPayload), crate::error::Error>> {
409 let ev = make_event(fields);
410 let ctx = ResolveContext {
411 pre_fetched_order_pdas: None,
412 };
413 DcaAdapter.classify_and_resolve_event(&ev, &ctx)
414 }
415
416 #[test]
417 fn classify_known_instructions_via_envelope() {
418 let cases = [
419 ("OpenDca", Some(EventType::Created)),
420 ("OpenDcaV2", Some(EventType::Created)),
421 ("InitiateFlashFill", Some(EventType::FillInitiated)),
422 ("InitiateDlmmFill", Some(EventType::FillInitiated)),
423 ("FulfillFlashFill", Some(EventType::FillCompleted)),
424 ("FulfillDlmmFill", Some(EventType::FillCompleted)),
425 ("CloseDca", Some(EventType::Closed)),
426 ("EndAndClose", Some(EventType::Closed)),
427 ("Transfer", None),
428 ("Deposit", None),
429 ("Withdraw", None),
430 ("WithdrawFees", None),
431 ("Unknown", None),
432 ];
433 for (name, expected) in cases {
434 let ix = RawInstruction {
435 id: 1,
436 signature: "sig".to_string(),
437 instruction_index: 0,
438 instruction_path: None,
439 program_id: "p".to_string(),
440 inner_program_id: "p".to_string(),
441 instruction_name: name.to_string(),
442 accounts: None,
443 args: None,
444 slot: 1,
445 };
446 assert_eq!(
447 DcaAdapter.classify_instruction(&ix),
448 expected,
449 "mismatch for {name}"
450 );
451 }
452 }
453
454 #[test]
455 fn resolve_fill_event_from_envelope() {
456 let fields = serde_json::json!({
457 "FilledEvent": {
458 "dca_key": "3nsTjVJTwwGvXqDRgqNCZAQKwt4QMVhHHqvyseCNR3YX",
459 "in_amount": 21_041_666_667_u64,
460 "out_amount": 569_529_644_u64,
461 "fee": 570_099_u64,
462 "fee_mint": "A7b",
463 "input_mint": "So1",
464 "output_mint": "A7b",
465 "user_key": "31o"
466 }
467 });
468 let (event_type, correlation, payload) = resolve(fields).unwrap().unwrap();
469 assert_eq!(event_type, EventType::FillCompleted);
470 let CorrelationOutcome::Correlated(pdas) = correlation else {
471 panic!("expected Correlated");
472 };
473 assert_eq!(pdas, vec!["3nsTjVJTwwGvXqDRgqNCZAQKwt4QMVhHHqvyseCNR3YX"]);
474 let EventPayload::DcaFill {
475 in_amount,
476 out_amount,
477 } = payload
478 else {
479 panic!("expected DcaFill");
480 };
481 assert_eq!(in_amount, 21_041_666_667);
482 assert_eq!(out_amount, 569_529_644);
483 }
484
485 #[test]
486 fn resolve_closed_event_completed() {
487 let fields = serde_json::json!({
488 "ClosedEvent": {
489 "dca_key": "pda1",
490 "user_closed": false,
491 "unfilled_amount": 0_u64,
492 "created_at": 0, "in_amount_per_cycle": 0, "in_deposited": 0,
493 "input_mint": "x", "output_mint": "y", "total_in_withdrawn": 0,
494 "total_out_withdrawn": 0, "user_key": "z", "cycle_frequency": 60
495 }
496 });
497 let (event_type, _, payload) = resolve(fields).unwrap().unwrap();
498 assert_eq!(event_type, EventType::Closed);
499 assert_eq!(
500 payload,
501 EventPayload::DcaClosed {
502 status: TerminalStatus::Completed
503 }
504 );
505 }
506
507 #[test]
508 fn resolve_opened_event_correlates() {
509 let fields = serde_json::json!({
510 "OpenedEvent": {
511 "dca_key": "my_pda",
512 "created_at": 0, "cycle_frequency": 60, "in_amount_per_cycle": 100,
513 "in_deposited": 500, "input_mint": "a", "output_mint": "b", "user_key": "c"
514 }
515 });
516 let (event_type, correlation, payload) = resolve(fields).unwrap().unwrap();
517 assert_eq!(event_type, EventType::Created);
518 assert_eq!(
519 correlation,
520 CorrelationOutcome::Correlated(vec!["my_pda".to_string()])
521 );
522 assert_eq!(payload, EventPayload::None);
523 }
524
525 #[test]
526 fn unknown_event_returns_none() {
527 let fields = serde_json::json!({"UnknownEvent": {"some_field": 1}});
528 assert!(resolve(fields).is_none());
529 }
530
531 #[test]
532 fn malformed_known_event_returns_error() {
533 let fields = serde_json::json!({
534 "FilledEvent": {
535 "dca_key": "pda",
536 "in_amount": "bad",
537 "out_amount": 1_u64
538 }
539 });
540 let result = resolve(fields).unwrap();
541 assert!(result.is_err());
542 }
543
544 #[test]
545 fn resolve_fill_event_rejects_amount_overflow() {
546 let fields = serde_json::json!({
547 "FilledEvent": {
548 "dca_key": "pda",
549 "in_amount": (i64::MAX as u64) + 1,
550 "out_amount": 1_u64
551 }
552 });
553 let result = resolve(fields).unwrap();
554 assert!(result.is_err());
555 }
556
557 #[test]
558 fn parse_create_args_rejects_overflow_amounts() {
559 let args = serde_json::json!({
560 "in_amount": (i64::MAX as u64) + 1,
561 "in_amount_per_cycle": 1_u64,
562 "cycle_frequency": 60_i64,
563 "min_out_amount": 1_u64,
564 "max_out_amount": 1_u64
565 });
566 assert!(DcaAdapter::parse_create_args(&args).is_err());
567 }
568
569 #[test]
570 fn parse_create_args_accepts_valid_payload() {
571 let args = serde_json::json!({
572 "in_amount": 1_000_u64,
573 "in_amount_per_cycle": 100_u64,
574 "cycle_frequency": 60_i64,
575 "min_out_amount": 10_u64,
576 "max_out_amount": 500_u64,
577 "start_at": 1_700_000_000_i64
578 });
579 let parsed = DcaAdapter::parse_create_args(&args).unwrap();
580 assert_eq!(parsed.in_amount, 1_000);
581 assert_eq!(parsed.in_amount_per_cycle, 100);
582 assert_eq!(parsed.cycle_frequency, 60);
583 assert_eq!(parsed.min_out_amount, Some(10));
584 assert_eq!(parsed.max_out_amount, Some(500));
585 assert_eq!(parsed.start_at, Some(1_700_000_000));
586 }
587
588 #[test]
589 fn parse_create_args_treats_zero_start_at_as_none() {
590 let args = serde_json::json!({
591 "in_amount": 1_000_u64,
592 "in_amount_per_cycle": 100_u64,
593 "cycle_frequency": 60_i64,
594 "start_at": 0_i64
595 });
596 let parsed = DcaAdapter::parse_create_args(&args).unwrap();
597 assert!(parsed.start_at.is_none(), "start_at: 0 should become None");
598 }
599
600 #[test]
601 fn parse_create_args_treats_negative_start_at_as_none() {
602 let args = serde_json::json!({
603 "in_amount": 1_000_u64,
604 "in_amount_per_cycle": 100_u64,
605 "cycle_frequency": 60_i64,
606 "start_at": -1_i64
607 });
608 let parsed = DcaAdapter::parse_create_args(&args).unwrap();
609 assert!(
610 parsed.start_at.is_none(),
611 "negative start_at should become None"
612 );
613 }
614
615 #[test]
616 fn parse_create_args_rejects_malformed_payload() {
617 let args = serde_json::json!({
618 "in_amount": "bad",
619 "in_amount_per_cycle": 100_u64,
620 "cycle_frequency": 60_i64
621 });
622 assert!(DcaAdapter::parse_create_args(&args).is_err());
623 }
624
625 #[test]
626 fn extract_order_pda_prefers_named_account() {
627 let accounts = vec![
628 account("idx0", None),
629 account("idx1", None),
630 account("named_dca", Some("dca")),
631 ];
632 let extracted = DcaAdapter::extract_order_pda(&accounts, "CloseDca").unwrap();
633 assert_eq!(extracted, "named_dca");
634 }
635
636 #[test]
637 fn extract_order_pda_uses_instruction_fallback_indexes() {
638 let open_accounts = vec![account("open_idx0", None)];
639 assert_eq!(
640 DcaAdapter::extract_order_pda(&open_accounts, "OpenDca").unwrap(),
641 "open_idx0"
642 );
643
644 let close_accounts = vec![account("ignore0", None), account("close_idx1", None)];
645 assert_eq!(
646 DcaAdapter::extract_order_pda(&close_accounts, "CloseDca").unwrap(),
647 "close_idx1"
648 );
649 }
650
651 #[test]
652 fn extract_order_pda_rejects_unknown_instruction() {
653 let err = DcaAdapter::extract_order_pda(&[account("a", None)], "Unknown").unwrap_err();
654 let Error::Protocol { reason } = err else {
655 panic!("expected protocol error");
656 };
657 assert_eq!(reason, "unknown DCA instruction: Unknown");
658 }
659
660 #[test]
661 fn extract_order_pda_rejects_out_of_bounds_fallback() {
662 let err = DcaAdapter::extract_order_pda(&[account("only0", None)], "CloseDca").unwrap_err();
663 let Error::Protocol { reason } = err else {
664 panic!("expected protocol error");
665 };
666 assert_eq!(reason, "DCA account index 1 out of bounds for CloseDca");
667 }
668
669 #[test]
670 fn extract_create_mints_prefers_named_accounts() {
671 let accounts = vec![
672 account("fallback_input", None),
673 account("fallback_output", None),
674 account("named_input", Some("input_mint")),
675 account("named_output", Some("output_mint")),
676 ];
677 let extracted = DcaAdapter::extract_create_mints(&accounts, "OpenDca").unwrap();
678 assert_eq!(extracted.input_mint, "named_input");
679 assert_eq!(extracted.output_mint, "named_output");
680 }
681
682 #[test]
683 fn extract_create_mints_uses_fallback_indexes_for_create_variants() {
684 let open_accounts = vec![
685 account("0", None),
686 account("1", None),
687 account("open_input", None),
688 account("open_output", None),
689 ];
690 let open = DcaAdapter::extract_create_mints(&open_accounts, "OpenDca").unwrap();
691 assert_eq!(open.input_mint, "open_input");
692 assert_eq!(open.output_mint, "open_output");
693
694 let open_v2_accounts = vec![
695 account("0", None),
696 account("1", None),
697 account("2", None),
698 account("open_v2_input", None),
699 account("open_v2_output", None),
700 ];
701 let open_v2 = DcaAdapter::extract_create_mints(&open_v2_accounts, "OpenDcaV2").unwrap();
702 assert_eq!(open_v2.input_mint, "open_v2_input");
703 assert_eq!(open_v2.output_mint, "open_v2_output");
704 }
705
706 #[test]
707 fn extract_create_mints_rejects_non_create_instruction() {
708 let err = DcaAdapter::extract_create_mints(&[], "CloseDca")
709 .err()
710 .expect("expected error");
711 let Error::Protocol { reason } = err else {
712 panic!("expected protocol error");
713 };
714 assert_eq!(reason, "not a DCA create instruction: CloseDca");
715 }
716
717 #[test]
718 fn extract_create_mints_rejects_missing_fallback_input_index() {
719 let err = DcaAdapter::extract_create_mints(&[], "OpenDca")
720 .err()
721 .expect("expected error");
722 let Error::Protocol { reason } = err else {
723 panic!("expected protocol error");
724 };
725 assert_eq!(reason, "DCA input_mint index 2 out of bounds");
726 }
727
728 #[test]
729 fn extract_create_mints_rejects_missing_fallback_output_index() {
730 let accounts = vec![account("0", None), account("1", None), account("2", None)];
731 let err = DcaAdapter::extract_create_mints(&accounts, "OpenDca")
732 .err()
733 .expect("expected error");
734 let Error::Protocol { reason } = err else {
735 panic!("expected protocol error");
736 };
737 assert_eq!(reason, "DCA output_mint index 3 out of bounds");
738 }
739
740 #[test]
741 fn resolve_deposit_event_from_envelope() {
742 let fields = serde_json::json!({
743 "DepositEvent": {
744 "dca_key": "deposit_pda_123",
745 "amount": 1_000_000_u64,
746 "user_key": "user123"
747 }
748 });
749 let (event_type, correlation, payload) = resolve(fields).unwrap().unwrap();
750 assert_eq!(event_type, EventType::Deposited);
751 assert_eq!(
752 correlation,
753 CorrelationOutcome::Correlated(vec!["deposit_pda_123".to_string()])
754 );
755 assert_eq!(payload, EventPayload::None);
756 }
757
758 #[cfg(feature = "wasm")]
759 #[test]
760 fn instruction_constants_match_classify() {
761 for (name, expected) in INSTRUCTION_EVENT_TYPES {
762 let ix = RawInstruction {
763 id: 1,
764 signature: "sig".to_string(),
765 instruction_index: 0,
766 instruction_path: None,
767 program_id: "p".to_string(),
768 inner_program_id: "p".to_string(),
769 instruction_name: name.to_string(),
770 accounts: None,
771 args: None,
772 slot: 1,
773 };
774 assert_eq!(
775 DcaAdapter.classify_instruction(&ix).as_ref(),
776 Some(expected),
777 "INSTRUCTION_EVENT_TYPES mismatch for {name}"
778 );
779 }
780 }
781
782 #[cfg(feature = "wasm")]
783 #[test]
784 fn event_constants_match_resolve() {
785 for (name, expected) in EVENT_EVENT_TYPES {
786 let fields = match *name {
787 "FilledEvent" => {
788 serde_json::json!({(*name): {"dca_key": "t", "in_amount": 1_u64, "out_amount": 1_u64}})
789 }
790 "ClosedEvent" => {
791 serde_json::json!({(*name): {"dca_key": "t", "user_closed": false, "unfilled_amount": 0_u64}})
792 }
793 _ => serde_json::json!({(*name): {"dca_key": "t"}}),
794 };
795 let result = resolve(fields);
796 let (event_type, _, _) = result.expect("should return Some").expect("should be Ok");
797 assert_eq!(
798 &event_type, expected,
799 "EVENT_EVENT_TYPES mismatch for {name}"
800 );
801 }
802 }
803
804 #[test]
805 fn mirror_enums_cover_all_carbon_variants() {
806 let instruction_variants = [
807 "OpenDca",
808 "OpenDcaV2",
809 "InitiateFlashFill",
810 "InitiateDlmmFill",
811 "FulfillFlashFill",
812 "FulfillDlmmFill",
813 "CloseDca",
814 "EndAndClose",
815 "Transfer",
816 "Deposit",
817 "Withdraw",
818 "WithdrawFees",
819 ];
820 for name in instruction_variants {
821 let json = serde_json::json!({ name: serde_json::Value::Null });
822 assert!(
823 serde_json::from_value::<DcaInstructionKind>(json).is_ok(),
824 "DcaInstructionKind missing variant: {name}"
825 );
826 }
827
828 let key_holder_variants = [
829 "OpenedEvent",
830 "CollectedFeeEvent",
831 "WithdrawEvent",
832 "DepositEvent",
833 ];
834 for name in key_holder_variants {
835 let json = serde_json::json!({ name: { "dca_key": "test" } });
836 assert!(
837 serde_json::from_value::<DcaEventEnvelope>(json).is_ok(),
838 "DcaEventEnvelope missing variant: {name}"
839 );
840 }
841
842 let filled = serde_json::json!({
843 "FilledEvent": { "dca_key": "t", "in_amount": 1_u64, "out_amount": 1_u64 }
844 });
845 assert!(serde_json::from_value::<DcaEventEnvelope>(filled).is_ok());
846
847 let closed = serde_json::json!({
848 "ClosedEvent": { "dca_key": "t", "user_closed": false, "unfilled_amount": 0_u64 }
849 });
850 assert!(serde_json::from_value::<DcaEventEnvelope>(closed).is_ok());
851 }
852}