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            program_id: "p".to_string(),
343            inner_program_id: "p".to_string(),
344            event_name: "test".to_string(),
345            fields: Some(fields),
346            slot: 1,
347        }
348    }
349
350    fn resolve(
351        fields: serde_json::Value,
352    ) -> Option<Result<(EventType, CorrelationOutcome, EventPayload), crate::error::Error>> {
353        let ev = make_event(fields);
354        let ctx = ResolveContext {
355            pre_fetched_order_pdas: None,
356        };
357        LimitV2Adapter.classify_and_resolve_event(&ev, &ctx)
358    }
359
360    #[test]
361    fn classify_known_instructions_via_envelope() {
362        let cases = [
363            ("InitializeOrder", Some(EventType::Created)),
364            ("PreFlashFillOrder", Some(EventType::FillInitiated)),
365            ("FlashFillOrder", Some(EventType::FillCompleted)),
366            ("CancelOrder", Some(EventType::Cancelled)),
367            ("UpdateFee", None),
368            ("WithdrawFee", None),
369            ("Unknown", None),
370        ];
371        for (name, expected) in cases {
372            let ix = RawInstruction {
373                id: 1,
374                signature: "sig".to_string(),
375                instruction_index: 0,
376                program_id: "p".to_string(),
377                inner_program_id: "p".to_string(),
378                instruction_name: name.to_string(),
379                accounts: None,
380                args: None,
381                slot: 1,
382            };
383            assert_eq!(
384                LimitV2Adapter.classify_instruction(&ix),
385                expected,
386                "mismatch for {name}"
387            );
388        }
389    }
390
391    #[test]
392    fn resolve_trade_event_from_envelope() {
393        let fields = serde_json::json!({
394            "TradeEvent": {
395                "order_key": "HkLZgYy93cEi3Fn96SvdWeJk8DNeHeU5wiNV5SeRLiJC",
396                "taker": "j1oeQoPeuEDmjvyMwBmCWexzCQup77kbKKxV59CnYbd",
397                "making_amount": 724_773_829_u64,
398                "taking_amount": 51_821_329_u64,
399                "remaining_making_amount": 89_147_181_051_u64,
400                "remaining_taking_amount": 6_374_023_074_u64
401            }
402        });
403        let (event_type, correlation, payload) = resolve(fields).unwrap().unwrap();
404        assert_eq!(event_type, EventType::FillCompleted);
405        let CorrelationOutcome::Correlated(pdas) = correlation else {
406            panic!("expected Correlated");
407        };
408        assert_eq!(pdas, vec!["HkLZgYy93cEi3Fn96SvdWeJk8DNeHeU5wiNV5SeRLiJC"]);
409        let EventPayload::LimitFill {
410            in_amount,
411            out_amount,
412            remaining_in_amount,
413            counterparty,
414        } = payload
415        else {
416            panic!("expected LimitFill");
417        };
418        assert_eq!(in_amount, 724_773_829);
419        assert_eq!(out_amount, 51_821_329);
420        assert_eq!(remaining_in_amount, 89_147_181_051);
421        assert_eq!(counterparty, "j1oeQoPeuEDmjvyMwBmCWexzCQup77kbKKxV59CnYbd");
422    }
423
424    #[test]
425    fn malformed_known_event_returns_error() {
426        let fields = serde_json::json!({
427            "TradeEvent": {
428                "order_key": "order",
429                "making_amount": "bad",
430                "taking_amount": 1_u64,
431                "remaining_making_amount": 0_u64,
432                "remaining_taking_amount": 0_u64
433            }
434        });
435        let result = resolve(fields).unwrap();
436        assert!(result.is_err());
437    }
438
439    #[test]
440    fn resolve_trade_event_rejects_amount_overflow() {
441        let fields = serde_json::json!({
442            "TradeEvent": {
443                "order_key": "order",
444                "making_amount": (i64::MAX as u64) + 1,
445                "taking_amount": 1_u64,
446                "remaining_making_amount": 0_u64,
447                "remaining_taking_amount": 0_u64
448            }
449        });
450        let result = resolve(fields).unwrap();
451        assert!(result.is_err());
452    }
453
454    #[test]
455    fn resolve_trade_event_defaults_missing_taker() {
456        let fields = serde_json::json!({
457            "TradeEvent": {
458                "order_key": "order",
459                "making_amount": 10_u64,
460                "taking_amount": 5_u64,
461                "remaining_making_amount": 1_u64,
462                "remaining_taking_amount": 0_u64
463            }
464        });
465        let (_, _, payload) = resolve(fields).unwrap().unwrap();
466        let EventPayload::LimitFill { counterparty, .. } = payload else {
467            panic!("expected LimitFill");
468        };
469        assert_eq!(counterparty, "unknown");
470    }
471
472    #[test]
473    fn unknown_event_returns_none() {
474        let fields = serde_json::json!({"UnknownEvent": {"some_field": 1}});
475        assert!(resolve(fields).is_none());
476    }
477
478    #[test]
479    fn parse_create_args_with_params_wrapper() {
480        let args = serde_json::json!({
481            "params": {
482                "making_amount": 1000_u64,
483                "taking_amount": 500_u64,
484                "unique_id": 42_u64,
485                "expired_at": 1_700_000_000_i64,
486                "fee_bps": 25_u16
487            }
488        });
489        let parsed = LimitV2Adapter::parse_create_args(&args).unwrap();
490        assert_eq!(parsed.making_amount, 1000);
491        assert_eq!(parsed.taking_amount, 500);
492        assert_eq!(parsed.unique_id, Some(42));
493        assert_eq!(parsed.expired_at, Some(1_700_000_000));
494        assert_eq!(parsed.fee_bps, Some(25));
495    }
496
497    #[test]
498    fn parse_create_args_without_params_wrapper() {
499        let args = serde_json::json!({
500            "making_amount": 2000_u64,
501            "taking_amount": 1000_u64
502        });
503        let parsed = LimitV2Adapter::parse_create_args(&args).unwrap();
504        assert_eq!(parsed.making_amount, 2000);
505        assert_eq!(parsed.taking_amount, 1000);
506        assert_eq!(parsed.unique_id, None);
507        assert_eq!(parsed.expired_at, None);
508        assert_eq!(parsed.fee_bps, None);
509    }
510
511    #[test]
512    fn parse_create_args_rejects_overflow_values() {
513        let args = serde_json::json!({
514            "making_amount": (i64::MAX as u64) + 1,
515            "taking_amount": 1_u64,
516            "unique_id": (i64::MAX as u64) + 1
517        });
518        assert!(LimitV2Adapter::parse_create_args(&args).is_err());
519    }
520
521    #[test]
522    fn parse_create_args_rejects_fee_bps_out_of_range() {
523        let args = serde_json::json!({
524            "making_amount": 1_u64,
525            "taking_amount": 1_u64,
526            "fee_bps": 65_535_u16
527        });
528        assert!(LimitV2Adapter::parse_create_args(&args).is_err());
529    }
530
531    #[test]
532    fn parse_create_args_rejects_malformed_payload() {
533        let args = serde_json::json!({
534            "making_amount": "bad",
535            "taking_amount": 1_u64
536        });
537        assert!(LimitV2Adapter::parse_create_args(&args).is_err());
538    }
539
540    #[test]
541    fn extract_order_pda_prefers_named_account() {
542        let accounts = vec![
543            account("idx1", None),
544            account("idx2", None),
545            account("named_order", Some("order")),
546        ];
547        let extracted = LimitV2Adapter::extract_order_pda(&accounts, "CancelOrder").unwrap();
548        assert_eq!(extracted, "named_order");
549    }
550
551    #[test]
552    fn extract_order_pda_uses_fallback_indexes() {
553        let init_accounts = vec![
554            account("0", None),
555            account("1", None),
556            account("init_idx2", None),
557        ];
558        assert_eq!(
559            LimitV2Adapter::extract_order_pda(&init_accounts, "InitializeOrder").unwrap(),
560            "init_idx2"
561        );
562
563        let pre_flash_accounts = vec![account("0", None), account("pre_flash_idx1", None)];
564        assert_eq!(
565            LimitV2Adapter::extract_order_pda(&pre_flash_accounts, "PreFlashFillOrder").unwrap(),
566            "pre_flash_idx1"
567        );
568    }
569
570    #[test]
571    fn extract_order_pda_rejects_unknown_instruction() {
572        let err = LimitV2Adapter::extract_order_pda(&[], "Unknown").unwrap_err();
573        let Error::Protocol { reason } = err else {
574            panic!("expected protocol error");
575        };
576        assert_eq!(reason, "unknown Limit v2 instruction: Unknown");
577    }
578
579    #[test]
580    fn extract_order_pda_rejects_out_of_bounds_index() {
581        let err = LimitV2Adapter::extract_order_pda(&[], "CancelOrder").unwrap_err();
582        let Error::Protocol { reason } = err else {
583            panic!("expected protocol error");
584        };
585        assert_eq!(
586            reason,
587            "Limit v2 account index 2 out of bounds for CancelOrder"
588        );
589    }
590
591    #[test]
592    fn extract_create_mints_prefers_named_accounts() {
593        let accounts = vec![
594            account("idx7", None),
595            account("idx8", None),
596            account("named_input", Some("input_mint")),
597            account("named_output", Some("output_mint")),
598        ];
599        let extracted = LimitV2Adapter::extract_create_mints(&accounts).unwrap();
600        assert_eq!(extracted.input_mint, "named_input");
601        assert_eq!(extracted.output_mint, "named_output");
602    }
603
604    #[test]
605    fn extract_create_mints_uses_fallback_indexes() {
606        let accounts = vec![
607            account("0", None),
608            account("1", None),
609            account("2", None),
610            account("3", None),
611            account("4", None),
612            account("5", None),
613            account("6", None),
614            account("fallback_input", None),
615            account("fallback_output", None),
616        ];
617        let extracted = LimitV2Adapter::extract_create_mints(&accounts).unwrap();
618        assert_eq!(extracted.input_mint, "fallback_input");
619        assert_eq!(extracted.output_mint, "fallback_output");
620    }
621
622    #[test]
623    fn extract_create_mints_rejects_missing_input_fallback_index() {
624        let err = LimitV2Adapter::extract_create_mints(&[])
625            .err()
626            .expect("expected error");
627        let Error::Protocol { reason } = err else {
628            panic!("expected protocol error");
629        };
630        assert_eq!(reason, "Limit v2 input_mint index 7 out of bounds");
631    }
632
633    #[test]
634    fn extract_create_mints_rejects_missing_output_fallback_index() {
635        let accounts = vec![
636            account("0", None),
637            account("1", None),
638            account("2", None),
639            account("3", None),
640            account("4", None),
641            account("5", None),
642            account("6", None),
643            account("fallback_input", None),
644        ];
645        let err = LimitV2Adapter::extract_create_mints(&accounts)
646            .err()
647            .expect("expected error");
648        let Error::Protocol { reason } = err else {
649            panic!("expected protocol error");
650        };
651        assert_eq!(reason, "Limit v2 output_mint index 8 out of bounds");
652    }
653
654    #[cfg(feature = "wasm")]
655    #[test]
656    fn instruction_constants_match_classify() {
657        for (name, expected) in INSTRUCTION_EVENT_TYPES {
658            let ix = RawInstruction {
659                id: 1,
660                signature: "sig".to_string(),
661                instruction_index: 0,
662                program_id: "p".to_string(),
663                inner_program_id: "p".to_string(),
664                instruction_name: name.to_string(),
665                accounts: None,
666                args: None,
667                slot: 1,
668            };
669            assert_eq!(
670                LimitV2Adapter.classify_instruction(&ix).as_ref(),
671                Some(expected),
672                "INSTRUCTION_EVENT_TYPES mismatch for {name}"
673            );
674        }
675    }
676
677    #[cfg(feature = "wasm")]
678    #[test]
679    fn event_constants_match_resolve() {
680        for (name, expected) in EVENT_EVENT_TYPES {
681            let fields = match *name {
682                "TradeEvent" => {
683                    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}})
684                }
685                _ => serde_json::json!({(*name): {"order_key": "t"}}),
686            };
687            let result = resolve(fields);
688            let (event_type, _, _) = result.expect("should return Some").expect("should be Ok");
689            assert_eq!(
690                &event_type, expected,
691                "EVENT_EVENT_TYPES mismatch for {name}"
692            );
693        }
694    }
695
696    #[test]
697    fn mirror_enums_cover_all_carbon_variants() {
698        let instruction_variants = [
699            "InitializeOrder",
700            "PreFlashFillOrder",
701            "FlashFillOrder",
702            "CancelOrder",
703            "UpdateFee",
704            "WithdrawFee",
705        ];
706        for name in instruction_variants {
707            let json = serde_json::json!({ name: serde_json::Value::Null });
708            assert!(
709                serde_json::from_value::<LimitV2InstructionKind>(json).is_ok(),
710                "LimitV2InstructionKind missing variant: {name}"
711            );
712        }
713
714        for name in ["CreateOrderEvent", "CancelOrderEvent"] {
715            let json = serde_json::json!({ name: { "order_key": "test" } });
716            assert!(
717                serde_json::from_value::<LimitV2EventEnvelope>(json).is_ok(),
718                "LimitV2EventEnvelope missing variant: {name}"
719            );
720        }
721
722        let trade = serde_json::json!({
723            "TradeEvent": { "order_key": "t", "making_amount": 1_u64, "taking_amount": 1_u64,
724                "remaining_making_amount": 0_u64, "remaining_taking_amount": 0_u64 }
725        });
726        assert!(serde_json::from_value::<LimitV2EventEnvelope>(trade).is_ok());
727    }
728}