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