1use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize};
4
5use crate::types::{IdlAmountHint, SteelDiscriminant};
6
7pub const IDL_NORMALIZATION_VERSION: u32 = 1;
12
13#[derive(Debug, Clone, Serialize)]
14pub struct IdlSnapshot {
15 pub name: String,
16 #[serde(default, skip_serializing_if = "Option::is_none", alias = "address")]
17 pub program_id: Option<String>,
18 pub version: String,
19 pub accounts: Vec<IdlAccountSnapshot>,
20 pub instructions: Vec<IdlInstructionSnapshot>,
21 #[serde(default)]
22 pub types: Vec<IdlTypeDefSnapshot>,
23 #[serde(default)]
24 pub events: Vec<IdlEventSnapshot>,
25 #[serde(default)]
26 pub errors: Vec<IdlErrorSnapshot>,
27 pub discriminant_size: usize,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
35#[serde(rename_all = "camelCase", try_from = "IdlSnapshotV1Wire")]
36pub struct IdlSnapshotV1 {
37 pub normalization_version: u32,
38 #[serde(flatten)]
39 pub snapshot: IdlSnapshot,
40}
41
42#[derive(Deserialize)]
43#[serde(rename_all = "camelCase")]
44struct IdlSnapshotV1Wire {
45 normalization_version: u32,
46 #[serde(flatten)]
47 snapshot: IdlSnapshot,
48}
49
50impl TryFrom<IdlSnapshotV1Wire> for IdlSnapshotV1 {
51 type Error = String;
52
53 fn try_from(value: IdlSnapshotV1Wire) -> Result<Self, Self::Error> {
54 if value.normalization_version != IDL_NORMALIZATION_VERSION {
55 return Err(format!(
56 "unsupported IDL normalization version {}; expected {}",
57 value.normalization_version, IDL_NORMALIZATION_VERSION
58 ));
59 }
60 Ok(Self {
61 normalization_version: value.normalization_version,
62 snapshot: value.snapshot,
63 })
64 }
65}
66
67impl IdlSnapshotV1 {
68 pub fn new(snapshot: IdlSnapshot) -> Self {
69 Self {
70 normalization_version: IDL_NORMALIZATION_VERSION,
71 snapshot,
72 }
73 }
74
75 pub fn into_legacy_snapshot(self) -> IdlSnapshot {
76 self.snapshot
77 }
78}
79
80impl<'de> Deserialize<'de> for IdlSnapshot {
81 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
82 where
83 D: Deserializer<'de>,
84 {
85 let value = serde_json::Value::deserialize(deserializer)?;
87
88 let discriminant_size = value
90 .get("instructions")
91 .and_then(|instrs| instrs.as_array())
92 .map(|instrs| {
93 if instrs.is_empty() {
94 return false;
95 }
96 instrs.iter().all(|ix| {
97 let discriminator = ix.get("discriminator");
98 let disc_len = discriminator
99 .and_then(|d| d.as_array())
100 .map(|a| a.len())
101 .unwrap_or(0);
102
103 let has_discriminant = ix
108 .get("discriminant")
109 .map(|v| !v.is_null())
110 .unwrap_or(false);
111 let has_discriminator = discriminator
112 .map(|d| {
113 !d.is_null() && d.as_array().map(|a| !a.is_empty()).unwrap_or(true)
114 })
115 .unwrap_or(false);
116
117 let is_steel_discriminant = has_discriminant && !has_discriminator;
119
120 let is_steel_short_discriminator = !has_discriminant && disc_len == 1;
124
125 is_steel_discriminant || is_steel_short_discriminator
126 })
127 })
128 .map(|is_steel| if is_steel { 1 } else { 8 })
129 .unwrap_or(8); let mut intermediate: IdlSnapshotIntermediate = serde_json::from_value(value)
133 .map_err(|e| DeError::custom(format!("Failed to deserialize IDL: {}", e)))?;
134 if intermediate.discriminant_size == 0 {
137 intermediate.discriminant_size = discriminant_size;
138 }
139
140 Ok(IdlSnapshot {
141 name: intermediate.name,
142 program_id: intermediate.program_id,
143 version: intermediate.version,
144 accounts: intermediate.accounts,
145 instructions: intermediate.instructions,
146 types: intermediate.types,
147 events: intermediate.events,
148 errors: intermediate.errors,
149 discriminant_size: intermediate.discriminant_size,
150 })
151 }
152}
153
154#[derive(Debug, Clone, Deserialize)]
156struct IdlSnapshotIntermediate {
157 pub name: String,
158 #[serde(default, alias = "address")]
159 pub program_id: Option<String>,
160 pub version: String,
161 pub accounts: Vec<IdlAccountSnapshot>,
162 pub instructions: Vec<IdlInstructionSnapshot>,
163 #[serde(default)]
164 pub types: Vec<IdlTypeDefSnapshot>,
165 #[serde(default)]
166 pub events: Vec<IdlEventSnapshot>,
167 #[serde(default)]
168 pub errors: Vec<IdlErrorSnapshot>,
169 #[serde(default)]
170 pub discriminant_size: usize,
171}
172
173#[derive(Debug, Clone, Serialize)]
174pub struct IdlAccountSnapshot {
175 pub name: String,
176 pub discriminator: Vec<u8>,
177 pub docs: Vec<String>,
178 pub serialization: Option<IdlSerializationSnapshot>,
179 pub fields: Vec<IdlFieldSnapshot>,
181 #[serde(skip_serializing_if = "Option::is_none")]
183 pub type_def: Option<IdlInlineTypeDef>,
184}
185
186#[derive(Deserialize)]
188struct IdlAccountSnapshotIntermediate {
189 pub name: String,
190 pub discriminator: Vec<u8>,
191 #[serde(default)]
192 pub docs: Vec<String>,
193 #[serde(default, skip_serializing_if = "Option::is_none")]
194 pub serialization: Option<IdlSerializationSnapshot>,
195 #[serde(default)]
196 pub fields: Vec<IdlFieldSnapshot>,
197 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
198 pub type_def: Option<IdlInlineTypeDef>,
199}
200
201impl<'de> Deserialize<'de> for IdlAccountSnapshot {
202 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
203 where
204 D: Deserializer<'de>,
205 {
206 let intermediate = IdlAccountSnapshotIntermediate::deserialize(deserializer)?;
207
208 let fields = if intermediate.fields.is_empty() {
210 if let Some(type_def) = intermediate.type_def.as_ref() {
211 type_def.fields.clone()
212 } else {
213 intermediate.fields
214 }
215 } else {
216 intermediate.fields
217 };
218
219 Ok(IdlAccountSnapshot {
220 name: intermediate.name,
221 discriminator: intermediate.discriminator,
222 docs: intermediate.docs,
223 serialization: intermediate.serialization,
224 fields,
225 type_def: intermediate.type_def,
226 })
227 }
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct IdlInlineTypeDef {
233 pub kind: String,
234 pub fields: Vec<IdlFieldSnapshot>,
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct IdlInstructionSnapshot {
239 pub name: String,
240 #[serde(default)]
241 pub discriminator: Vec<u8>,
242 #[serde(default)]
243 pub discriminant: Option<SteelDiscriminant>,
244 #[serde(default)]
245 pub docs: Vec<String>,
246 pub accounts: Vec<IdlInstructionAccountSnapshot>,
247 pub args: Vec<IdlFieldSnapshot>,
248}
249
250impl IdlInstructionSnapshot {
251 pub fn get_discriminator(&self) -> Vec<u8> {
254 if !self.discriminator.is_empty() {
255 return self.discriminator.clone();
256 }
257
258 if let Some(disc) = &self.discriminant {
259 match u8::try_from(disc.value) {
260 Ok(value) => return vec![value],
261 Err(_) => {
262 tracing::warn!(
263 instruction = %self.name,
264 value = disc.value,
265 "Steel discriminant exceeds u8::MAX; falling back to Anchor hash"
266 );
267 }
268 }
269 }
270
271 crate::discriminator::anchor_discriminator(&format!(
272 "global:{}",
273 crate::utils::to_snake_case(&self.name)
274 ))
275 }
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize)]
279pub struct IdlInstructionAccountSnapshot {
280 pub name: String,
281 #[serde(default)]
282 pub writable: bool,
283 #[serde(default)]
284 pub signer: bool,
285 #[serde(default)]
286 pub optional: bool,
287 #[serde(default)]
288 pub address: Option<String>,
289 #[serde(default)]
290 pub docs: Vec<String>,
291}
292
293#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct IdlFieldSnapshot {
295 pub name: String,
296 #[serde(rename = "type")]
297 pub type_: IdlTypeSnapshot,
298 #[serde(
299 default,
300 skip_serializing_if = "Option::is_none",
301 rename = "amountHint"
302 )]
303 pub amount_hint: Option<IdlAmountHint>,
304}
305
306#[derive(Debug, Clone, Serialize, Deserialize)]
307#[serde(untagged)]
308pub enum IdlTypeSnapshot {
309 Simple(String),
310 Array(IdlArrayTypeSnapshot),
311 Option(IdlOptionTypeSnapshot),
312 Vec(IdlVecTypeSnapshot),
313 HashMap(IdlHashMapTypeSnapshot),
314 Defined(IdlDefinedTypeSnapshot),
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct IdlHashMapTypeSnapshot {
319 #[serde(rename = "hashMap", deserialize_with = "deserialize_hash_map")]
320 pub hash_map: (Box<IdlTypeSnapshot>, Box<IdlTypeSnapshot>),
321}
322
323fn deserialize_hash_map<'de, D>(
324 deserializer: D,
325) -> Result<(Box<IdlTypeSnapshot>, Box<IdlTypeSnapshot>), D::Error>
326where
327 D: Deserializer<'de>,
328{
329 use serde::de::Error;
330 let values: Vec<IdlTypeSnapshot> = Vec::deserialize(deserializer)?;
331 if values.len() != 2 {
332 return Err(D::Error::custom("hashMap must have exactly 2 elements"));
333 }
334 let mut iter = values.into_iter();
335 Ok((
336 Box::new(iter.next().expect("length checked")),
337 Box::new(iter.next().expect("length checked")),
338 ))
339}
340
341#[derive(Debug, Clone, Serialize, Deserialize)]
342pub struct IdlArrayTypeSnapshot {
343 pub array: Vec<IdlArrayElementSnapshot>,
344}
345
346#[derive(Debug, Clone, Serialize, Deserialize)]
347#[serde(untagged)]
348pub enum IdlArrayElementSnapshot {
349 Type(IdlTypeSnapshot),
350 TypeName(String),
351 Size(u32),
352}
353
354#[derive(Debug, Clone, Serialize, Deserialize)]
355pub struct IdlOptionTypeSnapshot {
356 pub option: Box<IdlTypeSnapshot>,
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize)]
360pub struct IdlVecTypeSnapshot {
361 pub vec: Box<IdlTypeSnapshot>,
362}
363
364#[derive(Debug, Clone, Serialize, Deserialize)]
365pub struct IdlDefinedTypeSnapshot {
366 pub defined: IdlDefinedInnerSnapshot,
367}
368
369#[derive(Debug, Clone, Serialize, Deserialize)]
370#[serde(untagged)]
371pub enum IdlDefinedInnerSnapshot {
372 Named { name: String },
373 Simple(String),
374}
375
376#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
377#[serde(rename_all = "lowercase")]
378pub enum IdlSerializationSnapshot {
379 Borsh,
380 Bytemuck,
381 #[serde(alias = "bytemuckunsafe")]
382 BytemuckUnsafe,
383}
384
385#[derive(Debug, Clone, Serialize, Deserialize)]
386pub struct IdlTypeDefSnapshot {
387 pub name: String,
388 #[serde(default)]
389 pub docs: Vec<String>,
390 #[serde(default, skip_serializing_if = "Option::is_none")]
391 pub serialization: Option<IdlSerializationSnapshot>,
392 #[serde(rename = "type")]
393 pub type_def: IdlTypeDefKindSnapshot,
394}
395
396#[derive(Debug, Clone, Serialize, Deserialize)]
397#[serde(untagged)]
398pub enum IdlTypeDefKindSnapshot {
399 Struct {
400 kind: String,
401 fields: Vec<IdlFieldSnapshot>,
402 },
403 TupleStruct {
404 kind: String,
405 fields: Vec<IdlTypeSnapshot>,
406 },
407 Enum {
408 kind: String,
409 variants: Vec<IdlEnumVariantSnapshot>,
410 },
411}
412
413#[derive(Debug, Clone, Serialize, Deserialize)]
414pub struct IdlEnumVariantSnapshot {
415 pub name: String,
416 #[serde(default, skip_serializing_if = "Vec::is_empty")]
419 pub fields: Vec<IdlEnumVariantFieldSnapshot>,
420}
421
422#[derive(Debug, Clone, Serialize, Deserialize)]
425#[serde(untagged)]
426pub enum IdlEnumVariantFieldSnapshot {
427 Named(IdlFieldSnapshot),
428 Tuple(IdlTypeSnapshot),
429}
430
431#[derive(Debug, Clone, Serialize, Deserialize)]
432pub struct IdlEventSnapshot {
433 pub name: String,
434 pub discriminator: Vec<u8>,
435 #[serde(default)]
436 pub docs: Vec<String>,
437 #[serde(default)]
438 pub fields: Vec<IdlFieldSnapshot>,
439}
440
441#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
442pub struct IdlErrorSnapshot {
443 pub code: u32,
444 pub name: String,
445 #[serde(default, skip_serializing_if = "Option::is_none")]
446 pub msg: Option<String>,
447}
448
449pub fn normalize_idl_snapshot(idl: &crate::types::IdlSpec) -> IdlSnapshot {
451 use crate::types::IdlTypeDefKind;
452
453 let mut types: Vec<IdlTypeDefSnapshot> = idl
454 .types
455 .iter()
456 .map(|typedef| IdlTypeDefSnapshot {
457 name: typedef.name.clone(),
458 docs: typedef.docs.clone(),
459 serialization: typedef.serialization.as_ref().map(snapshot_serialization),
460 type_def: snapshot_type_def(&typedef.type_def),
461 })
462 .collect();
463
464 for account in &idl.accounts {
465 if types.iter().any(|typedef| typedef.name == account.name) {
466 continue;
467 }
468 if let Some(type_def) = &account.type_def {
469 types.push(IdlTypeDefSnapshot {
470 name: account.name.clone(),
471 docs: account.docs.clone(),
472 serialization: None,
473 type_def: snapshot_type_def(type_def),
474 });
475 }
476 }
477
478 let uses_steel_discriminant = idl.instructions.iter().any(|instruction| {
479 instruction.discriminant.is_some() && instruction.discriminator.is_empty()
480 });
481 let discriminant_size = if uses_steel_discriminant { 1 } else { 8 };
482 let program_id = idl.address.clone().or_else(|| {
483 idl.metadata
484 .as_ref()
485 .and_then(|metadata| metadata.address.clone())
486 });
487
488 IdlSnapshot {
489 name: idl.get_name().to_string(),
490 program_id,
491 version: idl.get_version().to_string(),
492 accounts: idl
493 .accounts
494 .iter()
495 .map(|account| {
496 let serialization = idl
497 .types
498 .iter()
499 .find(|typedef| typedef.name == account.name)
500 .and_then(|typedef| typedef.serialization.as_ref())
501 .map(snapshot_serialization);
502 let fields =
503 account
504 .type_def
505 .as_ref()
506 .map_or_else(Vec::new, |type_def| match type_def {
507 IdlTypeDefKind::Struct { fields, .. } => {
508 fields.iter().map(snapshot_field).collect()
509 }
510 _ => Vec::new(),
511 });
512
513 IdlAccountSnapshot {
514 name: account.name.clone(),
515 discriminator: account.get_discriminator(),
516 docs: account.docs.clone(),
517 serialization,
518 fields,
519 type_def: None,
520 }
521 })
522 .collect(),
523 instructions: idl
524 .instructions
525 .iter()
526 .map(|instruction| IdlInstructionSnapshot {
527 name: instruction.name.clone(),
528 discriminator: instruction.get_discriminator(),
529 discriminant: instruction.discriminant.clone(),
530 docs: instruction.docs.clone(),
531 accounts: instruction
532 .flattened_accounts()
533 .iter()
534 .map(|account| IdlInstructionAccountSnapshot {
535 name: account.name.clone(),
536 writable: account.is_mut,
537 signer: account.is_signer,
538 optional: account.optional,
539 address: account.address.clone(),
540 docs: account.docs.clone(),
541 })
542 .collect(),
543 args: instruction.args.iter().map(snapshot_field).collect(),
544 })
545 .collect(),
546 types,
547 events: idl
548 .events
549 .iter()
550 .map(|event| IdlEventSnapshot {
551 name: event.name.clone(),
552 discriminator: event.get_discriminator(),
553 docs: event.docs.clone(),
554 fields: event.fields.iter().map(snapshot_field).collect(),
555 })
556 .collect(),
557 errors: idl
558 .errors
559 .iter()
560 .map(|error| IdlErrorSnapshot {
561 code: error.code,
562 name: error.name.clone(),
563 msg: error.msg.clone(),
564 })
565 .collect(),
566 discriminant_size,
567 }
568}
569
570pub fn normalize_idl_snapshot_v1(idl: &crate::types::IdlSpec) -> IdlSnapshotV1 {
572 IdlSnapshotV1::new(normalize_idl_snapshot(idl))
573}
574
575fn snapshot_serialization(
576 serialization: &crate::types::IdlSerialization,
577) -> IdlSerializationSnapshot {
578 match serialization {
579 crate::types::IdlSerialization::Borsh => IdlSerializationSnapshot::Borsh,
580 crate::types::IdlSerialization::Bytemuck => IdlSerializationSnapshot::Bytemuck,
581 crate::types::IdlSerialization::BytemuckUnsafe => IdlSerializationSnapshot::BytemuckUnsafe,
582 }
583}
584
585fn snapshot_field(field: &crate::types::IdlField) -> IdlFieldSnapshot {
586 IdlFieldSnapshot {
587 name: field.name.clone(),
588 type_: snapshot_type(&field.type_),
589 amount_hint: field.amount_hint.clone(),
590 }
591}
592
593fn snapshot_type_def(type_def: &crate::types::IdlTypeDefKind) -> IdlTypeDefKindSnapshot {
594 match type_def {
595 crate::types::IdlTypeDefKind::Struct { kind, fields } => IdlTypeDefKindSnapshot::Struct {
596 kind: kind.clone(),
597 fields: fields.iter().map(snapshot_field).collect(),
598 },
599 crate::types::IdlTypeDefKind::TupleStruct { kind, fields } => {
600 IdlTypeDefKindSnapshot::TupleStruct {
601 kind: kind.clone(),
602 fields: fields.iter().map(snapshot_type).collect(),
603 }
604 }
605 crate::types::IdlTypeDefKind::Enum { kind, variants } => IdlTypeDefKindSnapshot::Enum {
606 kind: kind.clone(),
607 variants: variants.iter().map(snapshot_enum_variant).collect(),
608 },
609 }
610}
611
612fn snapshot_enum_variant(variant: &crate::types::IdlEnumVariant) -> IdlEnumVariantSnapshot {
613 IdlEnumVariantSnapshot {
614 name: variant.name.clone(),
615 fields: variant
616 .fields
617 .iter()
618 .map(|field| match field {
619 crate::types::IdlEnumVariantField::Named(named) => {
620 IdlEnumVariantFieldSnapshot::Named(snapshot_field(named))
621 }
622 crate::types::IdlEnumVariantField::Tuple(tuple) => {
623 IdlEnumVariantFieldSnapshot::Tuple(snapshot_type(tuple))
624 }
625 })
626 .collect(),
627 }
628}
629
630fn snapshot_type(idl_type: &crate::types::IdlType) -> IdlTypeSnapshot {
631 match idl_type {
632 crate::types::IdlType::Simple(simple) => IdlTypeSnapshot::Simple(simple.clone()),
633 crate::types::IdlType::Array(array) => IdlTypeSnapshot::Array(IdlArrayTypeSnapshot {
634 array: array
635 .array
636 .iter()
637 .map(|element| match element {
638 crate::types::IdlTypeArrayElement::Nested(ty) => {
639 IdlArrayElementSnapshot::Type(snapshot_type(ty))
640 }
641 crate::types::IdlTypeArrayElement::Type(type_name) => {
642 IdlArrayElementSnapshot::TypeName(type_name.clone())
643 }
644 crate::types::IdlTypeArrayElement::Size(size) => {
645 IdlArrayElementSnapshot::Size(*size)
646 }
647 })
648 .collect(),
649 }),
650 crate::types::IdlType::Option(option) => IdlTypeSnapshot::Option(IdlOptionTypeSnapshot {
651 option: Box::new(snapshot_type(&option.option)),
652 }),
653 crate::types::IdlType::Vec(vec_type) => IdlTypeSnapshot::Vec(IdlVecTypeSnapshot {
654 vec: Box::new(snapshot_type(&vec_type.vec)),
655 }),
656 crate::types::IdlType::Defined(defined) => {
657 IdlTypeSnapshot::Defined(IdlDefinedTypeSnapshot {
658 defined: match &defined.defined {
659 crate::types::IdlTypeDefinedInner::Named { name } => {
660 IdlDefinedInnerSnapshot::Named { name: name.clone() }
661 }
662 crate::types::IdlTypeDefinedInner::Simple(simple) => {
663 IdlDefinedInnerSnapshot::Simple(simple.clone())
664 }
665 },
666 })
667 }
668 crate::types::IdlType::HashMap(hash_map) => {
669 IdlTypeSnapshot::HashMap(IdlHashMapTypeSnapshot {
670 hash_map: (
671 Box::new(snapshot_type(&hash_map.hash_map.0)),
672 Box::new(snapshot_type(&hash_map.hash_map.1)),
673 ),
674 })
675 }
676 }
677}
678
679#[cfg(test)]
680mod tests {
681 use super::*;
682
683 #[test]
684 fn test_fieldless_enum_variant_wire_format_unchanged() {
685 let fieldless = IdlEnumVariantSnapshot {
689 name: "Active".to_string(),
690 fields: vec![],
691 };
692 assert_eq!(
693 serde_json::to_string(&fieldless).unwrap(),
694 r#"{"name":"Active"}"#
695 );
696
697 let parsed: IdlEnumVariantSnapshot = serde_json::from_str(r#"{"name":"Active"}"#).unwrap();
699 assert!(parsed.fields.is_empty());
700
701 let fielded = IdlTypeDefKindSnapshot::Enum {
704 kind: "enum".to_string(),
705 variants: vec![IdlEnumVariantSnapshot {
706 name: "Sunset".to_string(),
707 fields: vec![
708 IdlEnumVariantFieldSnapshot::Named(IdlFieldSnapshot {
709 name: "endTs".to_string(),
710 type_: IdlTypeSnapshot::Simple("i64".to_string()),
711 amount_hint: None,
712 }),
713 IdlEnumVariantFieldSnapshot::Tuple(IdlTypeSnapshot::Simple("u8".to_string())),
714 ],
715 }],
716 };
717 let json = serde_json::to_string(&fielded).unwrap();
718 let parsed: IdlTypeDefKindSnapshot = serde_json::from_str(&json).unwrap();
719 match parsed {
720 IdlTypeDefKindSnapshot::Enum { variants, .. } => {
721 assert_eq!(variants[0].fields.len(), 2);
722 assert!(matches!(
723 variants[0].fields[0],
724 IdlEnumVariantFieldSnapshot::Named(_)
725 ));
726 assert!(matches!(
727 variants[0].fields[1],
728 IdlEnumVariantFieldSnapshot::Tuple(_)
729 ));
730 }
731 other => panic!("untagged round-trip misclassified enum as {:?}", other),
732 }
733 }
734
735 #[test]
736 fn test_snapshot_serde() {
737 let snapshot = IdlSnapshot {
738 name: "test_program".to_string(),
739 program_id: Some("11111111111111111111111111111111".to_string()),
740 version: "0.1.0".to_string(),
741 accounts: vec![IdlAccountSnapshot {
742 name: "ExampleAccount".to_string(),
743 discriminator: vec![1, 2, 3, 4, 5, 6, 7, 8],
744 docs: vec!["Example account".to_string()],
745 serialization: Some(IdlSerializationSnapshot::Borsh),
746 fields: vec![],
747 type_def: None,
748 }],
749 instructions: vec![IdlInstructionSnapshot {
750 name: "example_instruction".to_string(),
751 discriminator: vec![8, 7, 6, 5, 4, 3, 2, 1],
752 discriminant: None,
753 docs: vec!["Example instruction".to_string()],
754 accounts: vec![IdlInstructionAccountSnapshot {
755 name: "payer".to_string(),
756 writable: true,
757 signer: true,
758 optional: false,
759 address: None,
760 docs: vec![],
761 }],
762 args: vec![IdlFieldSnapshot {
763 name: "amount".to_string(),
764 type_: IdlTypeSnapshot::HashMap(IdlHashMapTypeSnapshot {
765 hash_map: (
766 Box::new(IdlTypeSnapshot::Simple("u64".to_string())),
767 Box::new(IdlTypeSnapshot::Simple("string".to_string())),
768 ),
769 }),
770 amount_hint: None,
771 }],
772 }],
773 types: vec![IdlTypeDefSnapshot {
774 name: "ExampleType".to_string(),
775 docs: vec![],
776 serialization: None,
777 type_def: IdlTypeDefKindSnapshot::Struct {
778 kind: "struct".to_string(),
779 fields: vec![IdlFieldSnapshot {
780 name: "value".to_string(),
781 type_: IdlTypeSnapshot::Simple("u64".to_string()),
782 amount_hint: None,
783 }],
784 },
785 }],
786 events: vec![IdlEventSnapshot {
787 name: "ExampleEvent".to_string(),
788 discriminator: vec![0, 0, 0, 0, 0, 0, 0, 1],
789 docs: vec![],
790 fields: vec![],
791 }],
792 errors: vec![IdlErrorSnapshot {
793 code: 6000,
794 name: "ExampleError".to_string(),
795 msg: Some("example".to_string()),
796 }],
797 discriminant_size: 8,
798 };
799
800 let serialized = serde_json::to_value(&snapshot).expect("serialize snapshot");
801 let deserialized: IdlSnapshot =
802 serde_json::from_value(serialized.clone()).expect("deserialize snapshot");
803 let round_trip = serde_json::to_value(&deserialized).expect("re-serialize snapshot");
804
805 assert_eq!(serialized, round_trip);
806 assert_eq!(deserialized.name, "test_program");
807 }
808
809 #[test]
810 fn test_hashmap_compat() {
811 let json = r#"{"hashMap":["u64","string"]}"#;
812 let parsed: IdlHashMapTypeSnapshot =
813 serde_json::from_str(json).expect("deserialize hashMap");
814
815 assert!(matches!(
816 parsed.hash_map.0.as_ref(),
817 IdlTypeSnapshot::Simple(value) if value == "u64"
818 ));
819 assert!(matches!(
820 parsed.hash_map.1.as_ref(),
821 IdlTypeSnapshot::Simple(value) if value == "string"
822 ));
823 }
824
825 #[test]
826 fn idl_snapshot_v1_rejects_unknown_normalization_versions() {
827 let value = serde_json::json!({
828 "normalizationVersion": 2,
829 "name": "demo",
830 "program_id": "11111111111111111111111111111111",
831 "version": "1.0.0",
832 "accounts": [],
833 "instructions": [],
834 "types": [],
835 "events": [],
836 "errors": [],
837 "discriminant_size": 8
838 });
839
840 let error = serde_json::from_value::<IdlSnapshotV1>(value)
841 .expect_err("unknown normalization version must fail");
842 assert!(error
843 .to_string()
844 .contains("unsupported IDL normalization version 2"));
845 }
846}