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
359 .map(|v| ProtocolHelpers::checked_u64_to_i64(v, "min_out_amount"))
360 .transpose()?,
361 max_out_amount: max_out_amount
362 .map(|v| ProtocolHelpers::checked_u64_to_i64(v, "max_out_amount"))
363 .transpose()?,
364 start_at,
365 })
366 }
367
368 #[cfg(all(test, feature = "native"))]
369 pub fn classify_decoded(
370 decoded: &carbon_jupiter_dca_decoder::instructions::JupiterDcaInstruction,
371 ) -> Option<EventType> {
372 use carbon_jupiter_dca_decoder::instructions::JupiterDcaInstruction;
373 match decoded {
374 JupiterDcaInstruction::OpenDca(_) | JupiterDcaInstruction::OpenDcaV2(_) => {
375 Some(EventType::Created)
376 }
377 JupiterDcaInstruction::InitiateFlashFill(_)
378 | JupiterDcaInstruction::InitiateDlmmFill(_) => Some(EventType::FillInitiated),
379 JupiterDcaInstruction::FulfillFlashFill(_)
380 | JupiterDcaInstruction::FulfillDlmmFill(_) => Some(EventType::FillCompleted),
381 JupiterDcaInstruction::CloseDca(_) | JupiterDcaInstruction::EndAndClose(_) => {
382 Some(EventType::Closed)
383 }
384 JupiterDcaInstruction::OpenedEvent(_) => Some(EventType::Created),
385 JupiterDcaInstruction::FilledEvent(_) => Some(EventType::FillCompleted),
386 JupiterDcaInstruction::ClosedEvent(_) => Some(EventType::Closed),
387 JupiterDcaInstruction::CollectedFeeEvent(_) => Some(EventType::FeeCollected),
388 JupiterDcaInstruction::WithdrawEvent(_) => Some(EventType::Withdrawn),
389 JupiterDcaInstruction::DepositEvent(_) => Some(EventType::Deposited),
390 JupiterDcaInstruction::Transfer(_)
391 | JupiterDcaInstruction::Deposit(_)
392 | JupiterDcaInstruction::Withdraw(_)
393 | JupiterDcaInstruction::WithdrawFees(_) => None,
394 }
395 }
396}
397
398#[cfg(test)]
399#[expect(clippy::unwrap_used, reason = "test assertions")]
400mod tests {
401 use super::*;
402 use crate::lifecycle::TerminalStatus;
403
404 fn account(pubkey: &str, name: Option<&str>) -> AccountInfo {
405 AccountInfo {
406 pubkey: pubkey.to_string(),
407 is_signer: false,
408 is_writable: false,
409 name: name.map(str::to_string),
410 }
411 }
412
413 fn make_event(fields: serde_json::Value) -> RawEvent {
414 RawEvent {
415 id: 1,
416 signature: "sig".to_string(),
417 event_index: 0,
418 program_id: "p".to_string(),
419 inner_program_id: "p".to_string(),
420 event_name: "test".to_string(),
421 fields: Some(fields),
422 slot: 1,
423 }
424 }
425
426 fn resolve(
427 fields: serde_json::Value,
428 ) -> Option<Result<(EventType, CorrelationOutcome, EventPayload), crate::error::Error>> {
429 let ev = make_event(fields);
430 let ctx = ResolveContext {
431 pre_fetched_order_pdas: None,
432 };
433 DcaAdapter.classify_and_resolve_event(&ev, &ctx)
434 }
435
436 #[test]
437 fn classify_known_instructions_via_envelope() {
438 let cases = [
439 ("OpenDca", Some(EventType::Created)),
440 ("OpenDcaV2", Some(EventType::Created)),
441 ("InitiateFlashFill", Some(EventType::FillInitiated)),
442 ("InitiateDlmmFill", Some(EventType::FillInitiated)),
443 ("FulfillFlashFill", Some(EventType::FillCompleted)),
444 ("FulfillDlmmFill", Some(EventType::FillCompleted)),
445 ("CloseDca", Some(EventType::Closed)),
446 ("EndAndClose", Some(EventType::Closed)),
447 ("Transfer", None),
448 ("Deposit", None),
449 ("Withdraw", None),
450 ("WithdrawFees", None),
451 ("Unknown", None),
452 ];
453 for (name, expected) in cases {
454 let ix = RawInstruction {
455 id: 1,
456 signature: "sig".to_string(),
457 instruction_index: 0,
458 program_id: "p".to_string(),
459 inner_program_id: "p".to_string(),
460 instruction_name: name.to_string(),
461 accounts: None,
462 args: None,
463 slot: 1,
464 };
465 assert_eq!(
466 DcaAdapter.classify_instruction(&ix),
467 expected,
468 "mismatch for {name}"
469 );
470 }
471 }
472
473 #[test]
474 fn resolve_fill_event_from_envelope() {
475 let fields = serde_json::json!({
476 "FilledEvent": {
477 "dca_key": "3nsTjVJTwwGvXqDRgqNCZAQKwt4QMVhHHqvyseCNR3YX",
478 "in_amount": 21_041_666_667_u64,
479 "out_amount": 569_529_644_u64,
480 "fee": 570_099_u64,
481 "fee_mint": "A7b",
482 "input_mint": "So1",
483 "output_mint": "A7b",
484 "user_key": "31o"
485 }
486 });
487 let (event_type, correlation, payload) = resolve(fields).unwrap().unwrap();
488 assert_eq!(event_type, EventType::FillCompleted);
489 let CorrelationOutcome::Correlated(pdas) = correlation else {
490 panic!("expected Correlated");
491 };
492 assert_eq!(pdas, vec!["3nsTjVJTwwGvXqDRgqNCZAQKwt4QMVhHHqvyseCNR3YX"]);
493 let EventPayload::DcaFill {
494 in_amount,
495 out_amount,
496 } = payload
497 else {
498 panic!("expected DcaFill");
499 };
500 assert_eq!(in_amount, 21_041_666_667);
501 assert_eq!(out_amount, 569_529_644);
502 }
503
504 #[test]
505 fn resolve_closed_event_completed() {
506 let fields = serde_json::json!({
507 "ClosedEvent": {
508 "dca_key": "pda1",
509 "user_closed": false,
510 "unfilled_amount": 0_u64,
511 "created_at": 0, "in_amount_per_cycle": 0, "in_deposited": 0,
512 "input_mint": "x", "output_mint": "y", "total_in_withdrawn": 0,
513 "total_out_withdrawn": 0, "user_key": "z", "cycle_frequency": 60
514 }
515 });
516 let (event_type, _, payload) = resolve(fields).unwrap().unwrap();
517 assert_eq!(event_type, EventType::Closed);
518 assert_eq!(
519 payload,
520 EventPayload::DcaClosed {
521 status: TerminalStatus::Completed
522 }
523 );
524 }
525
526 #[test]
527 fn resolve_opened_event_correlates() {
528 let fields = serde_json::json!({
529 "OpenedEvent": {
530 "dca_key": "my_pda",
531 "created_at": 0, "cycle_frequency": 60, "in_amount_per_cycle": 100,
532 "in_deposited": 500, "input_mint": "a", "output_mint": "b", "user_key": "c"
533 }
534 });
535 let (event_type, correlation, payload) = resolve(fields).unwrap().unwrap();
536 assert_eq!(event_type, EventType::Created);
537 assert_eq!(
538 correlation,
539 CorrelationOutcome::Correlated(vec!["my_pda".to_string()])
540 );
541 assert_eq!(payload, EventPayload::None);
542 }
543
544 #[test]
545 fn unknown_event_returns_none() {
546 let fields = serde_json::json!({"UnknownEvent": {"some_field": 1}});
547 assert!(resolve(fields).is_none());
548 }
549
550 #[test]
551 fn malformed_known_event_returns_error() {
552 let fields = serde_json::json!({
553 "FilledEvent": {
554 "dca_key": "pda",
555 "in_amount": "bad",
556 "out_amount": 1_u64
557 }
558 });
559 let result = resolve(fields).unwrap();
560 assert!(result.is_err());
561 }
562
563 #[test]
564 fn resolve_fill_event_rejects_amount_overflow() {
565 let fields = serde_json::json!({
566 "FilledEvent": {
567 "dca_key": "pda",
568 "in_amount": (i64::MAX as u64) + 1,
569 "out_amount": 1_u64
570 }
571 });
572 let result = resolve(fields).unwrap();
573 assert!(result.is_err());
574 }
575
576 #[test]
577 fn parse_create_args_rejects_overflow_amounts() {
578 let args = serde_json::json!({
579 "in_amount": (i64::MAX as u64) + 1,
580 "in_amount_per_cycle": 1_u64,
581 "cycle_frequency": 60_i64,
582 "min_out_amount": 1_u64,
583 "max_out_amount": 1_u64
584 });
585 assert!(DcaAdapter::parse_create_args(&args).is_err());
586 }
587
588 #[test]
589 fn parse_create_args_accepts_valid_payload() {
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 "min_out_amount": 10_u64,
595 "max_out_amount": 500_u64,
596 "start_at": 1_700_000_000_i64
597 });
598 let parsed = DcaAdapter::parse_create_args(&args).unwrap();
599 assert_eq!(parsed.in_amount, 1_000);
600 assert_eq!(parsed.in_amount_per_cycle, 100);
601 assert_eq!(parsed.cycle_frequency, 60);
602 assert_eq!(parsed.min_out_amount, Some(10));
603 assert_eq!(parsed.max_out_amount, Some(500));
604 assert_eq!(parsed.start_at, Some(1_700_000_000));
605 }
606
607 #[test]
608 fn parse_create_args_rejects_malformed_payload() {
609 let args = serde_json::json!({
610 "in_amount": "bad",
611 "in_amount_per_cycle": 100_u64,
612 "cycle_frequency": 60_i64
613 });
614 assert!(DcaAdapter::parse_create_args(&args).is_err());
615 }
616
617 #[test]
618 fn extract_order_pda_prefers_named_account() {
619 let accounts = vec![
620 account("idx0", None),
621 account("idx1", None),
622 account("named_dca", Some("dca")),
623 ];
624 let extracted = DcaAdapter::extract_order_pda(&accounts, "CloseDca").unwrap();
625 assert_eq!(extracted, "named_dca");
626 }
627
628 #[test]
629 fn extract_order_pda_uses_instruction_fallback_indexes() {
630 let open_accounts = vec![account("open_idx0", None)];
631 assert_eq!(
632 DcaAdapter::extract_order_pda(&open_accounts, "OpenDca").unwrap(),
633 "open_idx0"
634 );
635
636 let close_accounts = vec![account("ignore0", None), account("close_idx1", None)];
637 assert_eq!(
638 DcaAdapter::extract_order_pda(&close_accounts, "CloseDca").unwrap(),
639 "close_idx1"
640 );
641 }
642
643 #[test]
644 fn extract_order_pda_rejects_unknown_instruction() {
645 let err = DcaAdapter::extract_order_pda(&[account("a", None)], "Unknown").unwrap_err();
646 let Error::Protocol { reason } = err else {
647 panic!("expected protocol error");
648 };
649 assert_eq!(reason, "unknown DCA instruction: Unknown");
650 }
651
652 #[test]
653 fn extract_order_pda_rejects_out_of_bounds_fallback() {
654 let err = DcaAdapter::extract_order_pda(&[account("only0", None)], "CloseDca").unwrap_err();
655 let Error::Protocol { reason } = err else {
656 panic!("expected protocol error");
657 };
658 assert_eq!(reason, "DCA account index 1 out of bounds for CloseDca");
659 }
660
661 #[test]
662 fn extract_create_mints_prefers_named_accounts() {
663 let accounts = vec![
664 account("fallback_input", None),
665 account("fallback_output", None),
666 account("named_input", Some("input_mint")),
667 account("named_output", Some("output_mint")),
668 ];
669 let extracted = DcaAdapter::extract_create_mints(&accounts, "OpenDca").unwrap();
670 assert_eq!(extracted.input_mint, "named_input");
671 assert_eq!(extracted.output_mint, "named_output");
672 }
673
674 #[test]
675 fn extract_create_mints_uses_fallback_indexes_for_create_variants() {
676 let open_accounts = vec![
677 account("0", None),
678 account("1", None),
679 account("open_input", None),
680 account("open_output", None),
681 ];
682 let open = DcaAdapter::extract_create_mints(&open_accounts, "OpenDca").unwrap();
683 assert_eq!(open.input_mint, "open_input");
684 assert_eq!(open.output_mint, "open_output");
685
686 let open_v2_accounts = vec![
687 account("0", None),
688 account("1", None),
689 account("2", None),
690 account("open_v2_input", None),
691 account("open_v2_output", None),
692 ];
693 let open_v2 = DcaAdapter::extract_create_mints(&open_v2_accounts, "OpenDcaV2").unwrap();
694 assert_eq!(open_v2.input_mint, "open_v2_input");
695 assert_eq!(open_v2.output_mint, "open_v2_output");
696 }
697
698 #[test]
699 fn extract_create_mints_rejects_non_create_instruction() {
700 let err = DcaAdapter::extract_create_mints(&[], "CloseDca")
701 .err()
702 .expect("expected error");
703 let Error::Protocol { reason } = err else {
704 panic!("expected protocol error");
705 };
706 assert_eq!(reason, "not a DCA create instruction: CloseDca");
707 }
708
709 #[test]
710 fn extract_create_mints_rejects_missing_fallback_input_index() {
711 let err = DcaAdapter::extract_create_mints(&[], "OpenDca")
712 .err()
713 .expect("expected error");
714 let Error::Protocol { reason } = err else {
715 panic!("expected protocol error");
716 };
717 assert_eq!(reason, "DCA input_mint index 2 out of bounds");
718 }
719
720 #[test]
721 fn extract_create_mints_rejects_missing_fallback_output_index() {
722 let accounts = vec![account("0", None), account("1", None), account("2", None)];
723 let err = DcaAdapter::extract_create_mints(&accounts, "OpenDca")
724 .err()
725 .expect("expected error");
726 let Error::Protocol { reason } = err else {
727 panic!("expected protocol error");
728 };
729 assert_eq!(reason, "DCA output_mint index 3 out of bounds");
730 }
731
732 #[test]
733 fn resolve_deposit_event_from_envelope() {
734 let fields = serde_json::json!({
735 "DepositEvent": {
736 "dca_key": "deposit_pda_123",
737 "amount": 1_000_000_u64,
738 "user_key": "user123"
739 }
740 });
741 let (event_type, correlation, payload) = resolve(fields).unwrap().unwrap();
742 assert_eq!(event_type, EventType::Deposited);
743 assert_eq!(
744 correlation,
745 CorrelationOutcome::Correlated(vec!["deposit_pda_123".to_string()])
746 );
747 assert_eq!(payload, EventPayload::None);
748 }
749
750 #[cfg(feature = "wasm")]
751 #[test]
752 fn instruction_constants_match_classify() {
753 for (name, expected) in INSTRUCTION_EVENT_TYPES {
754 let ix = RawInstruction {
755 id: 1,
756 signature: "sig".to_string(),
757 instruction_index: 0,
758 program_id: "p".to_string(),
759 inner_program_id: "p".to_string(),
760 instruction_name: name.to_string(),
761 accounts: None,
762 args: None,
763 slot: 1,
764 };
765 assert_eq!(
766 DcaAdapter.classify_instruction(&ix).as_ref(),
767 Some(expected),
768 "INSTRUCTION_EVENT_TYPES mismatch for {name}"
769 );
770 }
771 }
772
773 #[cfg(feature = "wasm")]
774 #[test]
775 fn event_constants_match_resolve() {
776 for (name, expected) in EVENT_EVENT_TYPES {
777 let fields = match *name {
778 "FilledEvent" => {
779 serde_json::json!({(*name): {"dca_key": "t", "in_amount": 1_u64, "out_amount": 1_u64}})
780 }
781 "ClosedEvent" => {
782 serde_json::json!({(*name): {"dca_key": "t", "user_closed": false, "unfilled_amount": 0_u64}})
783 }
784 _ => serde_json::json!({(*name): {"dca_key": "t"}}),
785 };
786 let result = resolve(fields);
787 let (event_type, _, _) = result.expect("should return Some").expect("should be Ok");
788 assert_eq!(
789 &event_type, expected,
790 "EVENT_EVENT_TYPES mismatch for {name}"
791 );
792 }
793 }
794
795 #[test]
796 fn mirror_enums_cover_all_carbon_variants() {
797 let instruction_variants = [
798 "OpenDca",
799 "OpenDcaV2",
800 "InitiateFlashFill",
801 "InitiateDlmmFill",
802 "FulfillFlashFill",
803 "FulfillDlmmFill",
804 "CloseDca",
805 "EndAndClose",
806 "Transfer",
807 "Deposit",
808 "Withdraw",
809 "WithdrawFees",
810 ];
811 for name in instruction_variants {
812 let json = serde_json::json!({ name: serde_json::Value::Null });
813 assert!(
814 serde_json::from_value::<DcaInstructionKind>(json).is_ok(),
815 "DcaInstructionKind missing variant: {name}"
816 );
817 }
818
819 let key_holder_variants = [
820 "OpenedEvent",
821 "CollectedFeeEvent",
822 "WithdrawEvent",
823 "DepositEvent",
824 ];
825 for name in key_holder_variants {
826 let json = serde_json::json!({ name: { "dca_key": "test" } });
827 assert!(
828 serde_json::from_value::<DcaEventEnvelope>(json).is_ok(),
829 "DcaEventEnvelope missing variant: {name}"
830 );
831 }
832
833 let filled = serde_json::json!({
834 "FilledEvent": { "dca_key": "t", "in_amount": 1_u64, "out_amount": 1_u64 }
835 });
836 assert!(serde_json::from_value::<DcaEventEnvelope>(filled).is_ok());
837
838 let closed = serde_json::json!({
839 "ClosedEvent": { "dca_key": "t", "user_closed": false, "unfilled_amount": 0_u64 }
840 });
841 assert!(serde_json::from_value::<DcaEventEnvelope>(closed).is_ok());
842 }
843}