Skip to main content

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