Skip to main content

defi_tracker_lifecycle/protocols/
limit_v2.rs

1use crate::error::Error;
2use crate::lifecycle::adapters::{CorrelationOutcome, EventPayload, ProtocolAdapter};
3use crate::protocols::{AccountInfo, EventType, Protocol, ProtocolHelpers};
4use crate::types::{RawEvent, RawInstruction, ResolveContext};
5use strum::VariantNames;
6
7/// Serde-tagged envelope for Jupiter Limit Order v2 event variants.
8#[derive(serde::Deserialize, strum_macros::VariantNames)]
9pub enum LimitV2EventEnvelope {
10    CreateOrderEvent(OrderKeyHolder),
11    CancelOrderEvent(OrderKeyHolder),
12    TradeEvent(TradeEventFields),
13}
14
15/// Serde-tagged envelope for Jupiter Limit Order v2 instruction variants.
16#[derive(serde::Deserialize)]
17pub enum LimitV2InstructionKind {
18    InitializeOrder(serde_json::Value),
19    PreFlashFillOrder(serde_json::Value),
20    FlashFillOrder(serde_json::Value),
21    CancelOrder(serde_json::Value),
22    UpdateFee(serde_json::Value),
23    WithdrawFee(serde_json::Value),
24}
25
26#[cfg(feature = "wasm")]
27pub const INSTRUCTION_EVENT_TYPES: &[(&str, EventType)] = &[
28    ("InitializeOrder", EventType::Created),
29    ("PreFlashFillOrder", EventType::FillInitiated),
30    ("FlashFillOrder", EventType::FillCompleted),
31    ("CancelOrder", EventType::Cancelled),
32];
33
34#[cfg(feature = "wasm")]
35pub const EVENT_EVENT_TYPES: &[(&str, EventType)] = &[
36    ("CreateOrderEvent", EventType::Created),
37    ("CancelOrderEvent", EventType::Cancelled),
38    ("TradeEvent", EventType::FillCompleted),
39];
40
41#[cfg(feature = "wasm")]
42pub const CLOSED_VARIANTS: &[&str] = &[];
43
44/// Jupiter Limit Order v2 protocol adapter (zero-sized, stored as a static).
45#[derive(Debug)]
46pub struct LimitV2Adapter;
47
48/// Serde intermediate for events that only carry an `order_key`.
49#[derive(serde::Deserialize)]
50pub struct OrderKeyHolder {
51    order_key: String,
52}
53
54/// Serde intermediate for `TradeEvent` payload fields (v2 field names).
55#[derive(serde::Deserialize)]
56pub struct TradeEventFields {
57    order_key: String,
58    #[serde(default = "LimitV2Adapter::default_unknown")]
59    taker: String,
60    making_amount: u64,
61    taking_amount: u64,
62    remaining_making_amount: u64,
63    #[expect(dead_code, reason = "consumed by serde for completeness")]
64    remaining_taking_amount: u64,
65}
66
67/// Extracted Limit v2 trade event with checked-cast amounts.
68pub struct LimitV2TradeEvent {
69    pub order_pda: String,
70    pub taker: String,
71    pub in_amount: i64,
72    pub out_amount: i64,
73    pub remaining_in_amount: i64,
74    pub remaining_out_amount: i64,
75}
76
77/// Parsed arguments from an `InitializeOrder` instruction (Limit v2).
78///
79/// `fee_bps` and `unique_id` are v2-specific fields not present in v1.
80pub struct LimitV2CreateArgs {
81    pub unique_id: Option<i64>,
82    pub making_amount: i64,
83    pub taking_amount: i64,
84    pub expired_at: Option<i64>,
85    pub fee_bps: Option<i16>,
86}
87
88/// Input and output mint addresses extracted from a Limit v2 create instruction.
89pub struct LimitV2CreateMints {
90    pub input_mint: String,
91    pub output_mint: String,
92}
93
94#[derive(serde::Deserialize)]
95struct InitializeOrderParamsFields {
96    #[serde(default)]
97    unique_id: Option<u64>,
98    making_amount: u64,
99    taking_amount: u64,
100    expired_at: Option<i64>,
101    #[serde(default)]
102    fee_bps: Option<u16>,
103}
104
105#[derive(serde::Deserialize)]
106struct InitializeOrderWrapper {
107    params: InitializeOrderParamsFields,
108}
109
110impl ProtocolAdapter for LimitV2Adapter {
111    fn protocol(&self) -> Protocol {
112        Protocol::LimitV2
113    }
114
115    fn classify_instruction(&self, ix: &RawInstruction) -> Option<EventType> {
116        let wrapper = serde_json::json!({ &ix.instruction_name: ix.args });
117        let kind: LimitV2InstructionKind = serde_json::from_value(wrapper).ok()?;
118        match kind {
119            LimitV2InstructionKind::InitializeOrder(_) => Some(EventType::Created),
120            LimitV2InstructionKind::PreFlashFillOrder(_) => Some(EventType::FillInitiated),
121            LimitV2InstructionKind::FlashFillOrder(_) => Some(EventType::FillCompleted),
122            LimitV2InstructionKind::CancelOrder(_) => Some(EventType::Cancelled),
123            LimitV2InstructionKind::UpdateFee(_) | LimitV2InstructionKind::WithdrawFee(_) => None,
124        }
125    }
126
127    fn classify_and_resolve_event(
128        &self,
129        ev: &RawEvent,
130        _ctx: &ResolveContext,
131    ) -> Option<Result<(EventType, CorrelationOutcome, EventPayload), Error>> {
132        let fields = ev.fields.as_ref()?;
133        let envelope: LimitV2EventEnvelope = match serde_json::from_value(fields.clone()) {
134            Ok(e) => e,
135            Err(err) => {
136                if !ProtocolHelpers::contains_known_variant(fields, LimitV2EventEnvelope::VARIANTS)
137                {
138                    return None;
139                }
140                return Some(Err(Error::Protocol {
141                    reason: format!("failed to parse Limit v2 event payload: {err}"),
142                }));
143            }
144        };
145
146        Some(Self::resolve_event(envelope))
147    }
148}
149
150impl LimitV2Adapter {
151    fn default_unknown() -> String {
152        "unknown".to_string()
153    }
154
155    fn resolve_event(
156        envelope: LimitV2EventEnvelope,
157    ) -> Result<(EventType, CorrelationOutcome, EventPayload), Error> {
158        match envelope {
159            LimitV2EventEnvelope::CreateOrderEvent(OrderKeyHolder { order_key }) => Ok((
160                EventType::Created,
161                CorrelationOutcome::Correlated(vec![order_key]),
162                EventPayload::None,
163            )),
164            LimitV2EventEnvelope::CancelOrderEvent(OrderKeyHolder { order_key }) => Ok((
165                EventType::Cancelled,
166                CorrelationOutcome::Correlated(vec![order_key]),
167                EventPayload::None,
168            )),
169            LimitV2EventEnvelope::TradeEvent(TradeEventFields {
170                order_key,
171                taker,
172                making_amount,
173                taking_amount,
174                remaining_making_amount,
175                ..
176            }) => Ok((
177                EventType::FillCompleted,
178                CorrelationOutcome::Correlated(vec![order_key]),
179                EventPayload::LimitFill {
180                    in_amount: ProtocolHelpers::checked_u64_to_i64(making_amount, "making_amount")?,
181                    out_amount: ProtocolHelpers::checked_u64_to_i64(
182                        taking_amount,
183                        "taking_amount",
184                    )?,
185                    remaining_in_amount: ProtocolHelpers::checked_u64_to_i64(
186                        remaining_making_amount,
187                        "remaining_making_amount",
188                    )?,
189                    counterparty: taker,
190                },
191            )),
192        }
193    }
194
195    /// Extracts the order PDA from instruction accounts.
196    ///
197    /// Prefers the named `"order"` account; falls back to positional index per instruction variant.
198    pub fn extract_order_pda(
199        accounts: &[AccountInfo],
200        instruction_name: &str,
201    ) -> Result<String, Error> {
202        if let Some(acc) = ProtocolHelpers::find_account_by_name(accounts, "order") {
203            return Ok(acc.pubkey.clone());
204        }
205
206        let wrapper = serde_json::json!({ instruction_name: serde_json::Value::Null });
207        let kind: LimitV2InstructionKind =
208            serde_json::from_value(wrapper).map_err(|_| Error::Protocol {
209                reason: format!("unknown Limit v2 instruction: {instruction_name}"),
210            })?;
211
212        let idx = match kind {
213            LimitV2InstructionKind::InitializeOrder(_) => 2,
214            LimitV2InstructionKind::FlashFillOrder(_) | LimitV2InstructionKind::CancelOrder(_) => 2,
215            LimitV2InstructionKind::PreFlashFillOrder(_) => 1,
216            LimitV2InstructionKind::UpdateFee(_) | LimitV2InstructionKind::WithdrawFee(_) => {
217                return Err(Error::Protocol {
218                    reason: format!("Limit v2 instruction {instruction_name} has no order PDA"),
219                });
220            }
221        };
222
223        accounts
224            .get(idx)
225            .map(|a| a.pubkey.clone())
226            .ok_or_else(|| Error::Protocol {
227                reason: format!(
228                    "Limit v2 account index {idx} out of bounds for {instruction_name}"
229                ),
230            })
231    }
232
233    /// Extracts input/output mint addresses from a Limit v2 create instruction's accounts.
234    ///
235    /// Prefers named accounts; falls back to positional indexes 7 (input) and 8 (output).
236    pub fn extract_create_mints(accounts: &[AccountInfo]) -> Result<LimitV2CreateMints, Error> {
237        let by_name_input =
238            ProtocolHelpers::find_account_by_name(accounts, "input_mint").map(|a| a.pubkey.clone());
239        let by_name_output = ProtocolHelpers::find_account_by_name(accounts, "output_mint")
240            .map(|a| a.pubkey.clone());
241
242        if let (Some(input_mint), Some(output_mint)) = (by_name_input, by_name_output) {
243            return Ok(LimitV2CreateMints {
244                input_mint,
245                output_mint,
246            });
247        }
248
249        let input_mint =
250            accounts
251                .get(7)
252                .map(|a| a.pubkey.clone())
253                .ok_or_else(|| Error::Protocol {
254                    reason: "Limit v2 input_mint index 7 out of bounds".into(),
255                })?;
256        let output_mint =
257            accounts
258                .get(8)
259                .map(|a| a.pubkey.clone())
260                .ok_or_else(|| Error::Protocol {
261                    reason: "Limit v2 output_mint index 8 out of bounds".into(),
262                })?;
263
264        Ok(LimitV2CreateMints {
265            input_mint,
266            output_mint,
267        })
268    }
269
270    /// Parses `InitializeOrder` instruction args into checked [`LimitV2CreateArgs`].
271    ///
272    /// Handles both `{"params": {...}}` wrapper format and flat format.
273    pub fn parse_create_args(args: &serde_json::Value) -> Result<LimitV2CreateArgs, Error> {
274        let params =
275            if let Ok(wrapper) = serde_json::from_value::<InitializeOrderWrapper>(args.clone()) {
276                wrapper.params
277            } else {
278                serde_json::from_value::<InitializeOrderParamsFields>(args.clone()).map_err(
279                    |e| Error::Protocol {
280                        reason: format!("failed to parse Limit v2 create args: {e}"),
281                    },
282                )?
283            };
284
285        let InitializeOrderParamsFields {
286            unique_id,
287            making_amount,
288            taking_amount,
289            expired_at,
290            fee_bps,
291        } = params;
292
293        Ok(LimitV2CreateArgs {
294            unique_id: unique_id.and_then(ProtocolHelpers::optional_u64_to_i64),
295            making_amount: ProtocolHelpers::checked_u64_to_i64(making_amount, "making_amount")?,
296            taking_amount: ProtocolHelpers::checked_u64_to_i64(taking_amount, "taking_amount")?,
297            expired_at,
298            fee_bps: fee_bps
299                .map(|v| ProtocolHelpers::checked_u16_to_i16(v, "fee_bps"))
300                .transpose()?,
301        })
302    }
303
304    #[cfg(all(test, feature = "native"))]
305    pub fn classify_decoded(
306        decoded: &carbon_jupiter_limit_order_2_decoder::instructions::JupiterLimitOrder2Instruction,
307    ) -> Option<EventType> {
308        use carbon_jupiter_limit_order_2_decoder::instructions::JupiterLimitOrder2Instruction;
309        match decoded {
310            JupiterLimitOrder2Instruction::InitializeOrder(_) => Some(EventType::Created),
311            JupiterLimitOrder2Instruction::PreFlashFillOrder(_) => Some(EventType::FillInitiated),
312            JupiterLimitOrder2Instruction::FlashFillOrder(_) => Some(EventType::FillCompleted),
313            JupiterLimitOrder2Instruction::CancelOrder(_) => Some(EventType::Cancelled),
314            JupiterLimitOrder2Instruction::CreateOrderEvent(_) => Some(EventType::Created),
315            JupiterLimitOrder2Instruction::CancelOrderEvent(_) => Some(EventType::Cancelled),
316            JupiterLimitOrder2Instruction::TradeEvent(_) => Some(EventType::FillCompleted),
317            JupiterLimitOrder2Instruction::UpdateFee(_)
318            | JupiterLimitOrder2Instruction::WithdrawFee(_) => None,
319        }
320    }
321}
322
323#[cfg(test)]
324#[expect(clippy::unwrap_used, reason = "test assertions")]
325mod tests {
326    use super::*;
327
328    fn account(pubkey: &str, name: Option<&str>) -> AccountInfo {
329        AccountInfo {
330            pubkey: pubkey.to_string(),
331            is_signer: false,
332            is_writable: false,
333            name: name.map(str::to_string),
334        }
335    }
336
337    fn make_event(fields: serde_json::Value) -> RawEvent {
338        RawEvent {
339            id: 1,
340            signature: "sig".to_string(),
341            event_index: 0,
342            event_path: None,
343            program_id: "p".to_string(),
344            inner_program_id: "p".to_string(),
345            event_name: "test".to_string(),
346            fields: Some(fields),
347            slot: 1,
348        }
349    }
350
351    fn resolve(
352        fields: serde_json::Value,
353    ) -> Option<Result<(EventType, CorrelationOutcome, EventPayload), crate::error::Error>> {
354        let ev = make_event(fields);
355        let ctx = ResolveContext {
356            pre_fetched_order_pdas: None,
357        };
358        LimitV2Adapter.classify_and_resolve_event(&ev, &ctx)
359    }
360
361    #[test]
362    fn classify_known_instructions_via_envelope() {
363        let cases = [
364            ("InitializeOrder", Some(EventType::Created)),
365            ("PreFlashFillOrder", Some(EventType::FillInitiated)),
366            ("FlashFillOrder", Some(EventType::FillCompleted)),
367            ("CancelOrder", Some(EventType::Cancelled)),
368            ("UpdateFee", None),
369            ("WithdrawFee", None),
370            ("Unknown", None),
371        ];
372        for (name, expected) in cases {
373            let ix = RawInstruction {
374                id: 1,
375                signature: "sig".to_string(),
376                instruction_index: 0,
377                instruction_path: None,
378                program_id: "p".to_string(),
379                inner_program_id: "p".to_string(),
380                instruction_name: name.to_string(),
381                accounts: None,
382                args: None,
383                slot: 1,
384            };
385            assert_eq!(
386                LimitV2Adapter.classify_instruction(&ix),
387                expected,
388                "mismatch for {name}"
389            );
390        }
391    }
392
393    #[test]
394    fn resolve_trade_event_from_envelope() {
395        let fields = serde_json::json!({
396            "TradeEvent": {
397                "order_key": "HkLZgYy93cEi3Fn96SvdWeJk8DNeHeU5wiNV5SeRLiJC",
398                "taker": "j1oeQoPeuEDmjvyMwBmCWexzCQup77kbKKxV59CnYbd",
399                "making_amount": 724_773_829_u64,
400                "taking_amount": 51_821_329_u64,
401                "remaining_making_amount": 89_147_181_051_u64,
402                "remaining_taking_amount": 6_374_023_074_u64
403            }
404        });
405        let (event_type, correlation, payload) = resolve(fields).unwrap().unwrap();
406        assert_eq!(event_type, EventType::FillCompleted);
407        let CorrelationOutcome::Correlated(pdas) = correlation else {
408            panic!("expected Correlated");
409        };
410        assert_eq!(pdas, vec!["HkLZgYy93cEi3Fn96SvdWeJk8DNeHeU5wiNV5SeRLiJC"]);
411        let EventPayload::LimitFill {
412            in_amount,
413            out_amount,
414            remaining_in_amount,
415            counterparty,
416        } = payload
417        else {
418            panic!("expected LimitFill");
419        };
420        assert_eq!(in_amount, 724_773_829);
421        assert_eq!(out_amount, 51_821_329);
422        assert_eq!(remaining_in_amount, 89_147_181_051);
423        assert_eq!(counterparty, "j1oeQoPeuEDmjvyMwBmCWexzCQup77kbKKxV59CnYbd");
424    }
425
426    #[test]
427    fn malformed_known_event_returns_error() {
428        let fields = serde_json::json!({
429            "TradeEvent": {
430                "order_key": "order",
431                "making_amount": "bad",
432                "taking_amount": 1_u64,
433                "remaining_making_amount": 0_u64,
434                "remaining_taking_amount": 0_u64
435            }
436        });
437        let result = resolve(fields).unwrap();
438        assert!(result.is_err());
439    }
440
441    #[test]
442    fn resolve_trade_event_rejects_amount_overflow() {
443        let fields = serde_json::json!({
444            "TradeEvent": {
445                "order_key": "order",
446                "making_amount": (i64::MAX as u64) + 1,
447                "taking_amount": 1_u64,
448                "remaining_making_amount": 0_u64,
449                "remaining_taking_amount": 0_u64
450            }
451        });
452        let result = resolve(fields).unwrap();
453        assert!(result.is_err());
454    }
455
456    #[test]
457    fn resolve_trade_event_defaults_missing_taker() {
458        let fields = serde_json::json!({
459            "TradeEvent": {
460                "order_key": "order",
461                "making_amount": 10_u64,
462                "taking_amount": 5_u64,
463                "remaining_making_amount": 1_u64,
464                "remaining_taking_amount": 0_u64
465            }
466        });
467        let (_, _, payload) = resolve(fields).unwrap().unwrap();
468        let EventPayload::LimitFill { counterparty, .. } = payload else {
469            panic!("expected LimitFill");
470        };
471        assert_eq!(counterparty, "unknown");
472    }
473
474    #[test]
475    fn unknown_event_returns_none() {
476        let fields = serde_json::json!({"UnknownEvent": {"some_field": 1}});
477        assert!(resolve(fields).is_none());
478    }
479
480    #[test]
481    fn parse_create_args_with_params_wrapper() {
482        let args = serde_json::json!({
483            "params": {
484                "making_amount": 1000_u64,
485                "taking_amount": 500_u64,
486                "unique_id": 42_u64,
487                "expired_at": 1_700_000_000_i64,
488                "fee_bps": 25_u16
489            }
490        });
491        let parsed = LimitV2Adapter::parse_create_args(&args).unwrap();
492        assert_eq!(parsed.making_amount, 1000);
493        assert_eq!(parsed.taking_amount, 500);
494        assert_eq!(parsed.unique_id, Some(42));
495        assert_eq!(parsed.expired_at, Some(1_700_000_000));
496        assert_eq!(parsed.fee_bps, Some(25));
497    }
498
499    #[test]
500    fn parse_create_args_without_params_wrapper() {
501        let args = serde_json::json!({
502            "making_amount": 2000_u64,
503            "taking_amount": 1000_u64
504        });
505        let parsed = LimitV2Adapter::parse_create_args(&args).unwrap();
506        assert_eq!(parsed.making_amount, 2000);
507        assert_eq!(parsed.taking_amount, 1000);
508        assert_eq!(parsed.unique_id, None);
509        assert_eq!(parsed.expired_at, None);
510        assert_eq!(parsed.fee_bps, None);
511    }
512
513    #[test]
514    fn parse_create_args_rejects_overflow_values() {
515        let args = serde_json::json!({
516            "making_amount": (i64::MAX as u64) + 1,
517            "taking_amount": 1_u64,
518            "unique_id": (i64::MAX as u64) + 1
519        });
520        assert!(LimitV2Adapter::parse_create_args(&args).is_err());
521    }
522
523    #[test]
524    fn parse_create_args_rejects_fee_bps_out_of_range() {
525        let args = serde_json::json!({
526            "making_amount": 1_u64,
527            "taking_amount": 1_u64,
528            "fee_bps": 65_535_u16
529        });
530        assert!(LimitV2Adapter::parse_create_args(&args).is_err());
531    }
532
533    #[test]
534    fn parse_create_args_rejects_malformed_payload() {
535        let args = serde_json::json!({
536            "making_amount": "bad",
537            "taking_amount": 1_u64
538        });
539        assert!(LimitV2Adapter::parse_create_args(&args).is_err());
540    }
541
542    #[test]
543    fn extract_order_pda_prefers_named_account() {
544        let accounts = vec![
545            account("idx1", None),
546            account("idx2", None),
547            account("named_order", Some("order")),
548        ];
549        let extracted = LimitV2Adapter::extract_order_pda(&accounts, "CancelOrder").unwrap();
550        assert_eq!(extracted, "named_order");
551    }
552
553    #[test]
554    fn extract_order_pda_uses_fallback_indexes() {
555        let init_accounts = vec![
556            account("0", None),
557            account("1", None),
558            account("init_idx2", None),
559        ];
560        assert_eq!(
561            LimitV2Adapter::extract_order_pda(&init_accounts, "InitializeOrder").unwrap(),
562            "init_idx2"
563        );
564
565        let pre_flash_accounts = vec![account("0", None), account("pre_flash_idx1", None)];
566        assert_eq!(
567            LimitV2Adapter::extract_order_pda(&pre_flash_accounts, "PreFlashFillOrder").unwrap(),
568            "pre_flash_idx1"
569        );
570    }
571
572    #[test]
573    fn extract_order_pda_rejects_unknown_instruction() {
574        let err = LimitV2Adapter::extract_order_pda(&[], "Unknown").unwrap_err();
575        let Error::Protocol { reason } = err else {
576            panic!("expected protocol error");
577        };
578        assert_eq!(reason, "unknown Limit v2 instruction: Unknown");
579    }
580
581    #[test]
582    fn extract_order_pda_rejects_out_of_bounds_index() {
583        let err = LimitV2Adapter::extract_order_pda(&[], "CancelOrder").unwrap_err();
584        let Error::Protocol { reason } = err else {
585            panic!("expected protocol error");
586        };
587        assert_eq!(
588            reason,
589            "Limit v2 account index 2 out of bounds for CancelOrder"
590        );
591    }
592
593    #[test]
594    fn extract_create_mints_prefers_named_accounts() {
595        let accounts = vec![
596            account("idx7", None),
597            account("idx8", None),
598            account("named_input", Some("input_mint")),
599            account("named_output", Some("output_mint")),
600        ];
601        let extracted = LimitV2Adapter::extract_create_mints(&accounts).unwrap();
602        assert_eq!(extracted.input_mint, "named_input");
603        assert_eq!(extracted.output_mint, "named_output");
604    }
605
606    #[test]
607    fn extract_create_mints_uses_fallback_indexes() {
608        let accounts = vec![
609            account("0", None),
610            account("1", None),
611            account("2", None),
612            account("3", None),
613            account("4", None),
614            account("5", None),
615            account("6", None),
616            account("fallback_input", None),
617            account("fallback_output", None),
618        ];
619        let extracted = LimitV2Adapter::extract_create_mints(&accounts).unwrap();
620        assert_eq!(extracted.input_mint, "fallback_input");
621        assert_eq!(extracted.output_mint, "fallback_output");
622    }
623
624    #[test]
625    fn extract_create_mints_rejects_missing_input_fallback_index() {
626        let err = LimitV2Adapter::extract_create_mints(&[])
627            .err()
628            .expect("expected error");
629        let Error::Protocol { reason } = err else {
630            panic!("expected protocol error");
631        };
632        assert_eq!(reason, "Limit v2 input_mint index 7 out of bounds");
633    }
634
635    #[test]
636    fn extract_create_mints_rejects_missing_output_fallback_index() {
637        let accounts = vec![
638            account("0", None),
639            account("1", None),
640            account("2", None),
641            account("3", None),
642            account("4", None),
643            account("5", None),
644            account("6", None),
645            account("fallback_input", None),
646        ];
647        let err = LimitV2Adapter::extract_create_mints(&accounts)
648            .err()
649            .expect("expected error");
650        let Error::Protocol { reason } = err else {
651            panic!("expected protocol error");
652        };
653        assert_eq!(reason, "Limit v2 output_mint index 8 out of bounds");
654    }
655
656    #[cfg(feature = "wasm")]
657    #[test]
658    fn instruction_constants_match_classify() {
659        for (name, expected) in INSTRUCTION_EVENT_TYPES {
660            let ix = RawInstruction {
661                id: 1,
662                signature: "sig".to_string(),
663                instruction_index: 0,
664                instruction_path: None,
665                program_id: "p".to_string(),
666                inner_program_id: "p".to_string(),
667                instruction_name: name.to_string(),
668                accounts: None,
669                args: None,
670                slot: 1,
671            };
672            assert_eq!(
673                LimitV2Adapter.classify_instruction(&ix).as_ref(),
674                Some(expected),
675                "INSTRUCTION_EVENT_TYPES mismatch for {name}"
676            );
677        }
678    }
679
680    #[cfg(feature = "wasm")]
681    #[test]
682    fn event_constants_match_resolve() {
683        for (name, expected) in EVENT_EVENT_TYPES {
684            let fields = match *name {
685                "TradeEvent" => {
686                    serde_json::json!({(*name): {"order_key": "t", "making_amount": 1_u64, "taking_amount": 1_u64, "remaining_making_amount": 0_u64, "remaining_taking_amount": 0_u64}})
687                }
688                _ => serde_json::json!({(*name): {"order_key": "t"}}),
689            };
690            let result = resolve(fields);
691            let (event_type, _, _) = result.expect("should return Some").expect("should be Ok");
692            assert_eq!(
693                &event_type, expected,
694                "EVENT_EVENT_TYPES mismatch for {name}"
695            );
696        }
697    }
698
699    #[test]
700    fn mirror_enums_cover_all_carbon_variants() {
701        let instruction_variants = [
702            "InitializeOrder",
703            "PreFlashFillOrder",
704            "FlashFillOrder",
705            "CancelOrder",
706            "UpdateFee",
707            "WithdrawFee",
708        ];
709        for name in instruction_variants {
710            let json = serde_json::json!({ name: serde_json::Value::Null });
711            assert!(
712                serde_json::from_value::<LimitV2InstructionKind>(json).is_ok(),
713                "LimitV2InstructionKind missing variant: {name}"
714            );
715        }
716
717        for name in ["CreateOrderEvent", "CancelOrderEvent"] {
718            let json = serde_json::json!({ name: { "order_key": "test" } });
719            assert!(
720                serde_json::from_value::<LimitV2EventEnvelope>(json).is_ok(),
721                "LimitV2EventEnvelope missing variant: {name}"
722            );
723        }
724
725        let trade = serde_json::json!({
726            "TradeEvent": { "order_key": "t", "making_amount": 1_u64, "taking_amount": 1_u64,
727                "remaining_making_amount": 0_u64, "remaining_taking_amount": 0_u64 }
728        });
729        assert!(serde_json::from_value::<LimitV2EventEnvelope>(trade).is_ok());
730    }
731}