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