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