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#[derive(serde::Deserialize, strum_macros::VariantNames)]
9pub enum LimitV1EventEnvelope {
10 CreateOrderEvent(OrderKeyHolder),
11 CancelOrderEvent(OrderKeyHolder),
12 TradeEvent(TradeEventFields),
13}
14
15#[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#[derive(Debug)]
48pub struct LimitV1Adapter;
49
50#[derive(serde::Deserialize)]
52pub struct OrderKeyHolder {
53 order_key: String,
54}
55
56#[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
75pub 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
85pub struct LimitV1CreateArgs {
87 pub making_amount: i64,
88 pub taking_amount: i64,
89 pub expired_at: Option<i64>,
90}
91
92pub 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 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 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 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}