1use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize};
4
5use crate::types::{IdlAmountHint, SteelDiscriminant};
6
7#[derive(Debug, Clone, Serialize)]
8pub struct IdlSnapshot {
9 pub name: String,
10 #[serde(default, skip_serializing_if = "Option::is_none", alias = "address")]
11 pub program_id: Option<String>,
12 pub version: String,
13 pub accounts: Vec<IdlAccountSnapshot>,
14 pub instructions: Vec<IdlInstructionSnapshot>,
15 #[serde(default)]
16 pub types: Vec<IdlTypeDefSnapshot>,
17 #[serde(default)]
18 pub events: Vec<IdlEventSnapshot>,
19 #[serde(default)]
20 pub errors: Vec<IdlErrorSnapshot>,
21 pub discriminant_size: usize,
22}
23
24impl<'de> Deserialize<'de> for IdlSnapshot {
25 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
26 where
27 D: Deserializer<'de>,
28 {
29 let value = serde_json::Value::deserialize(deserializer)?;
31
32 let discriminant_size = value
34 .get("instructions")
35 .and_then(|instrs| instrs.as_array())
36 .map(|instrs| {
37 if instrs.is_empty() {
38 return false;
39 }
40 instrs.iter().all(|ix| {
41 let discriminator = ix.get("discriminator");
42 let disc_len = discriminator
43 .and_then(|d| d.as_array())
44 .map(|a| a.len())
45 .unwrap_or(0);
46
47 let has_discriminant = ix
52 .get("discriminant")
53 .map(|v| !v.is_null())
54 .unwrap_or(false);
55 let has_discriminator = discriminator
56 .map(|d| {
57 !d.is_null() && d.as_array().map(|a| !a.is_empty()).unwrap_or(true)
58 })
59 .unwrap_or(false);
60
61 let is_steel_discriminant = has_discriminant && !has_discriminator;
63
64 let is_steel_short_discriminator = !has_discriminant && disc_len == 1;
68
69 is_steel_discriminant || is_steel_short_discriminator
70 })
71 })
72 .map(|is_steel| if is_steel { 1 } else { 8 })
73 .unwrap_or(8); let mut intermediate: IdlSnapshotIntermediate = serde_json::from_value(value)
77 .map_err(|e| DeError::custom(format!("Failed to deserialize IDL: {}", e)))?;
78 if intermediate.discriminant_size == 0 {
81 intermediate.discriminant_size = discriminant_size;
82 }
83
84 Ok(IdlSnapshot {
85 name: intermediate.name,
86 program_id: intermediate.program_id,
87 version: intermediate.version,
88 accounts: intermediate.accounts,
89 instructions: intermediate.instructions,
90 types: intermediate.types,
91 events: intermediate.events,
92 errors: intermediate.errors,
93 discriminant_size: intermediate.discriminant_size,
94 })
95 }
96}
97
98#[derive(Debug, Clone, Deserialize)]
100struct IdlSnapshotIntermediate {
101 pub name: String,
102 #[serde(default, alias = "address")]
103 pub program_id: Option<String>,
104 pub version: String,
105 pub accounts: Vec<IdlAccountSnapshot>,
106 pub instructions: Vec<IdlInstructionSnapshot>,
107 #[serde(default)]
108 pub types: Vec<IdlTypeDefSnapshot>,
109 #[serde(default)]
110 pub events: Vec<IdlEventSnapshot>,
111 #[serde(default)]
112 pub errors: Vec<IdlErrorSnapshot>,
113 #[serde(default)]
114 pub discriminant_size: usize,
115}
116
117#[derive(Debug, Clone, Serialize)]
118pub struct IdlAccountSnapshot {
119 pub name: String,
120 pub discriminator: Vec<u8>,
121 pub docs: Vec<String>,
122 pub serialization: Option<IdlSerializationSnapshot>,
123 pub fields: Vec<IdlFieldSnapshot>,
125 #[serde(skip_serializing_if = "Option::is_none")]
127 pub type_def: Option<IdlInlineTypeDef>,
128}
129
130#[derive(Deserialize)]
132struct IdlAccountSnapshotIntermediate {
133 pub name: String,
134 pub discriminator: Vec<u8>,
135 #[serde(default)]
136 pub docs: Vec<String>,
137 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub serialization: Option<IdlSerializationSnapshot>,
139 #[serde(default)]
140 pub fields: Vec<IdlFieldSnapshot>,
141 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
142 pub type_def: Option<IdlInlineTypeDef>,
143}
144
145impl<'de> Deserialize<'de> for IdlAccountSnapshot {
146 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
147 where
148 D: Deserializer<'de>,
149 {
150 let intermediate = IdlAccountSnapshotIntermediate::deserialize(deserializer)?;
151
152 let fields = if intermediate.fields.is_empty() {
154 if let Some(type_def) = intermediate.type_def.as_ref() {
155 type_def.fields.clone()
156 } else {
157 intermediate.fields
158 }
159 } else {
160 intermediate.fields
161 };
162
163 Ok(IdlAccountSnapshot {
164 name: intermediate.name,
165 discriminator: intermediate.discriminator,
166 docs: intermediate.docs,
167 serialization: intermediate.serialization,
168 fields,
169 type_def: intermediate.type_def,
170 })
171 }
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct IdlInlineTypeDef {
177 pub kind: String,
178 pub fields: Vec<IdlFieldSnapshot>,
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct IdlInstructionSnapshot {
183 pub name: String,
184 #[serde(default)]
185 pub discriminator: Vec<u8>,
186 #[serde(default)]
187 pub discriminant: Option<SteelDiscriminant>,
188 #[serde(default)]
189 pub docs: Vec<String>,
190 pub accounts: Vec<IdlInstructionAccountSnapshot>,
191 pub args: Vec<IdlFieldSnapshot>,
192}
193
194impl IdlInstructionSnapshot {
195 pub fn get_discriminator(&self) -> Vec<u8> {
198 if !self.discriminator.is_empty() {
199 return self.discriminator.clone();
200 }
201
202 if let Some(disc) = &self.discriminant {
203 match u8::try_from(disc.value) {
204 Ok(value) => return vec![value],
205 Err(_) => {
206 tracing::warn!(
207 instruction = %self.name,
208 value = disc.value,
209 "Steel discriminant exceeds u8::MAX; falling back to Anchor hash"
210 );
211 }
212 }
213 }
214
215 crate::discriminator::anchor_discriminator(&format!(
216 "global:{}",
217 crate::utils::to_snake_case(&self.name)
218 ))
219 }
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize)]
223pub struct IdlInstructionAccountSnapshot {
224 pub name: String,
225 #[serde(default)]
226 pub writable: bool,
227 #[serde(default)]
228 pub signer: bool,
229 #[serde(default)]
230 pub optional: bool,
231 #[serde(default)]
232 pub address: Option<String>,
233 #[serde(default)]
234 pub docs: Vec<String>,
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct IdlFieldSnapshot {
239 pub name: String,
240 #[serde(rename = "type")]
241 pub type_: IdlTypeSnapshot,
242 #[serde(
243 default,
244 skip_serializing_if = "Option::is_none",
245 rename = "amountHint"
246 )]
247 pub amount_hint: Option<IdlAmountHint>,
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize)]
251#[serde(untagged)]
252pub enum IdlTypeSnapshot {
253 Simple(String),
254 Array(IdlArrayTypeSnapshot),
255 Option(IdlOptionTypeSnapshot),
256 Vec(IdlVecTypeSnapshot),
257 HashMap(IdlHashMapTypeSnapshot),
258 Defined(IdlDefinedTypeSnapshot),
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct IdlHashMapTypeSnapshot {
263 #[serde(rename = "hashMap", deserialize_with = "deserialize_hash_map")]
264 pub hash_map: (Box<IdlTypeSnapshot>, Box<IdlTypeSnapshot>),
265}
266
267fn deserialize_hash_map<'de, D>(
268 deserializer: D,
269) -> Result<(Box<IdlTypeSnapshot>, Box<IdlTypeSnapshot>), D::Error>
270where
271 D: Deserializer<'de>,
272{
273 use serde::de::Error;
274 let values: Vec<IdlTypeSnapshot> = Vec::deserialize(deserializer)?;
275 if values.len() != 2 {
276 return Err(D::Error::custom("hashMap must have exactly 2 elements"));
277 }
278 let mut iter = values.into_iter();
279 Ok((
280 Box::new(iter.next().expect("length checked")),
281 Box::new(iter.next().expect("length checked")),
282 ))
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct IdlArrayTypeSnapshot {
287 pub array: Vec<IdlArrayElementSnapshot>,
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize)]
291#[serde(untagged)]
292pub enum IdlArrayElementSnapshot {
293 Type(IdlTypeSnapshot),
294 TypeName(String),
295 Size(u32),
296}
297
298#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct IdlOptionTypeSnapshot {
300 pub option: Box<IdlTypeSnapshot>,
301}
302
303#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct IdlVecTypeSnapshot {
305 pub vec: Box<IdlTypeSnapshot>,
306}
307
308#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct IdlDefinedTypeSnapshot {
310 pub defined: IdlDefinedInnerSnapshot,
311}
312
313#[derive(Debug, Clone, Serialize, Deserialize)]
314#[serde(untagged)]
315pub enum IdlDefinedInnerSnapshot {
316 Named { name: String },
317 Simple(String),
318}
319
320#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
321#[serde(rename_all = "lowercase")]
322pub enum IdlSerializationSnapshot {
323 Borsh,
324 Bytemuck,
325 #[serde(alias = "bytemuckunsafe")]
326 BytemuckUnsafe,
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize)]
330pub struct IdlTypeDefSnapshot {
331 pub name: String,
332 #[serde(default)]
333 pub docs: Vec<String>,
334 #[serde(default, skip_serializing_if = "Option::is_none")]
335 pub serialization: Option<IdlSerializationSnapshot>,
336 #[serde(rename = "type")]
337 pub type_def: IdlTypeDefKindSnapshot,
338}
339
340#[derive(Debug, Clone, Serialize, Deserialize)]
341#[serde(untagged)]
342pub enum IdlTypeDefKindSnapshot {
343 Struct {
344 kind: String,
345 fields: Vec<IdlFieldSnapshot>,
346 },
347 TupleStruct {
348 kind: String,
349 fields: Vec<IdlTypeSnapshot>,
350 },
351 Enum {
352 kind: String,
353 variants: Vec<IdlEnumVariantSnapshot>,
354 },
355}
356
357#[derive(Debug, Clone, Serialize, Deserialize)]
358pub struct IdlEnumVariantSnapshot {
359 pub name: String,
360 #[serde(default, skip_serializing_if = "Vec::is_empty")]
363 pub fields: Vec<IdlEnumVariantFieldSnapshot>,
364}
365
366#[derive(Debug, Clone, Serialize, Deserialize)]
369#[serde(untagged)]
370pub enum IdlEnumVariantFieldSnapshot {
371 Named(IdlFieldSnapshot),
372 Tuple(IdlTypeSnapshot),
373}
374
375#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct IdlEventSnapshot {
377 pub name: String,
378 pub discriminator: Vec<u8>,
379 #[serde(default)]
380 pub docs: Vec<String>,
381 #[serde(default)]
382 pub fields: Vec<IdlFieldSnapshot>,
383}
384
385#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
386pub struct IdlErrorSnapshot {
387 pub code: u32,
388 pub name: String,
389 #[serde(default, skip_serializing_if = "Option::is_none")]
390 pub msg: Option<String>,
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396
397 #[test]
398 fn test_fieldless_enum_variant_wire_format_unchanged() {
399 let fieldless = IdlEnumVariantSnapshot {
403 name: "Active".to_string(),
404 fields: vec![],
405 };
406 assert_eq!(
407 serde_json::to_string(&fieldless).unwrap(),
408 r#"{"name":"Active"}"#
409 );
410
411 let parsed: IdlEnumVariantSnapshot = serde_json::from_str(r#"{"name":"Active"}"#).unwrap();
413 assert!(parsed.fields.is_empty());
414
415 let fielded = IdlTypeDefKindSnapshot::Enum {
418 kind: "enum".to_string(),
419 variants: vec![IdlEnumVariantSnapshot {
420 name: "Sunset".to_string(),
421 fields: vec![
422 IdlEnumVariantFieldSnapshot::Named(IdlFieldSnapshot {
423 name: "endTs".to_string(),
424 type_: IdlTypeSnapshot::Simple("i64".to_string()),
425 amount_hint: None,
426 }),
427 IdlEnumVariantFieldSnapshot::Tuple(IdlTypeSnapshot::Simple("u8".to_string())),
428 ],
429 }],
430 };
431 let json = serde_json::to_string(&fielded).unwrap();
432 let parsed: IdlTypeDefKindSnapshot = serde_json::from_str(&json).unwrap();
433 match parsed {
434 IdlTypeDefKindSnapshot::Enum { variants, .. } => {
435 assert_eq!(variants[0].fields.len(), 2);
436 assert!(matches!(
437 variants[0].fields[0],
438 IdlEnumVariantFieldSnapshot::Named(_)
439 ));
440 assert!(matches!(
441 variants[0].fields[1],
442 IdlEnumVariantFieldSnapshot::Tuple(_)
443 ));
444 }
445 other => panic!("untagged round-trip misclassified enum as {:?}", other),
446 }
447 }
448
449 #[test]
450 fn test_snapshot_serde() {
451 let snapshot = IdlSnapshot {
452 name: "test_program".to_string(),
453 program_id: Some("11111111111111111111111111111111".to_string()),
454 version: "0.1.0".to_string(),
455 accounts: vec![IdlAccountSnapshot {
456 name: "ExampleAccount".to_string(),
457 discriminator: vec![1, 2, 3, 4, 5, 6, 7, 8],
458 docs: vec!["Example account".to_string()],
459 serialization: Some(IdlSerializationSnapshot::Borsh),
460 fields: vec![],
461 type_def: None,
462 }],
463 instructions: vec![IdlInstructionSnapshot {
464 name: "example_instruction".to_string(),
465 discriminator: vec![8, 7, 6, 5, 4, 3, 2, 1],
466 discriminant: None,
467 docs: vec!["Example instruction".to_string()],
468 accounts: vec![IdlInstructionAccountSnapshot {
469 name: "payer".to_string(),
470 writable: true,
471 signer: true,
472 optional: false,
473 address: None,
474 docs: vec![],
475 }],
476 args: vec![IdlFieldSnapshot {
477 name: "amount".to_string(),
478 type_: IdlTypeSnapshot::HashMap(IdlHashMapTypeSnapshot {
479 hash_map: (
480 Box::new(IdlTypeSnapshot::Simple("u64".to_string())),
481 Box::new(IdlTypeSnapshot::Simple("string".to_string())),
482 ),
483 }),
484 amount_hint: None,
485 }],
486 }],
487 types: vec![IdlTypeDefSnapshot {
488 name: "ExampleType".to_string(),
489 docs: vec![],
490 serialization: None,
491 type_def: IdlTypeDefKindSnapshot::Struct {
492 kind: "struct".to_string(),
493 fields: vec![IdlFieldSnapshot {
494 name: "value".to_string(),
495 type_: IdlTypeSnapshot::Simple("u64".to_string()),
496 amount_hint: None,
497 }],
498 },
499 }],
500 events: vec![IdlEventSnapshot {
501 name: "ExampleEvent".to_string(),
502 discriminator: vec![0, 0, 0, 0, 0, 0, 0, 1],
503 docs: vec![],
504 fields: vec![],
505 }],
506 errors: vec![IdlErrorSnapshot {
507 code: 6000,
508 name: "ExampleError".to_string(),
509 msg: Some("example".to_string()),
510 }],
511 discriminant_size: 8,
512 };
513
514 let serialized = serde_json::to_value(&snapshot).expect("serialize snapshot");
515 let deserialized: IdlSnapshot =
516 serde_json::from_value(serialized.clone()).expect("deserialize snapshot");
517 let round_trip = serde_json::to_value(&deserialized).expect("re-serialize snapshot");
518
519 assert_eq!(serialized, round_trip);
520 assert_eq!(deserialized.name, "test_program");
521 }
522
523 #[test]
524 fn test_hashmap_compat() {
525 let json = r#"{"hashMap":["u64","string"]}"#;
526 let parsed: IdlHashMapTypeSnapshot =
527 serde_json::from_str(json).expect("deserialize hashMap");
528
529 assert!(matches!(
530 parsed.hash_map.0.as_ref(),
531 IdlTypeSnapshot::Simple(value) if value == "u64"
532 ));
533 assert!(matches!(
534 parsed.hash_map.1.as_ref(),
535 IdlTypeSnapshot::Simple(value) if value == "string"
536 ));
537 }
538}