Skip to main content

hopper_schema/
codama.rs

1//! # Codama Projection Module
2//!
3//! JSON formatting for the three-tier schema export:
4//!
5//! ```text
6//! ProgramManifest  ⊃  ProgramIdl  ⊃  CodamaProjection
7//!       (rich)         (public)         (interop)
8//! ```
9//!
10//! All formatters use `core::fmt::Write` so they work in `#![no_std]`.
11
12use core::fmt;
13
14use crate::{
15    ArgDescriptor, CodamaProjection, CompatibilityPair, EventDescriptor, FieldDescriptor,
16    IdlAccountEntry, InstructionDescriptor, LayoutFingerprint, LayoutManifest, MigrationPolicy,
17    PdaSeedHint, PolicyDescriptor, ProgramIdl, ProgramManifest,
18};
19
20// ---------------------------------------------------------------------------
21// Shared JSON helpers
22// ---------------------------------------------------------------------------
23
24fn write_json_str(f: &mut fmt::Formatter<'_>, s: &str) -> fmt::Result {
25    write!(f, "\"")?;
26    for c in s.chars() {
27        match c {
28            '"' => write!(f, "\\\"")?,
29            '\\' => write!(f, "\\\\")?,
30            '\n' => write!(f, "\\n")?,
31            '\r' => write!(f, "\\r")?,
32            '\t' => write!(f, "\\t")?,
33            _ => write!(f, "{}", c)?,
34        }
35    }
36    write!(f, "\"")
37}
38
39fn write_hex_json(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result {
40    write!(f, "\"")?;
41    for b in bytes {
42        write!(f, "{:02x}", b)?;
43    }
44    write!(f, "\"")
45}
46
47fn write_indent(f: &mut fmt::Formatter<'_>, level: usize) -> fmt::Result {
48    for _ in 0..level {
49        write!(f, "  ")?;
50    }
51    Ok(())
52}
53
54fn write_str_array(f: &mut fmt::Formatter<'_>, items: &[&str], indent: usize) -> fmt::Result {
55    if items.is_empty() {
56        return write!(f, "[]");
57    }
58    writeln!(f, "[")?;
59    for (i, s) in items.iter().enumerate() {
60        write_indent(f, indent + 1)?;
61        write_json_str(f, s)?;
62        if i + 1 < items.len() {
63            writeln!(f, ",")?;
64        } else {
65            writeln!(f)?;
66        }
67    }
68    write_indent(f, indent)?;
69    write!(f, "]")
70}
71
72// ---------------------------------------------------------------------------
73// FieldDescriptor JSON
74// ---------------------------------------------------------------------------
75
76fn write_field_json(
77    f: &mut fmt::Formatter<'_>,
78    field: &FieldDescriptor,
79    indent: usize,
80) -> fmt::Result {
81    write_indent(f, indent)?;
82    write!(f, "{{ \"name\": ")?;
83    write_json_str(f, field.name)?;
84    write!(f, ", \"type\": ")?;
85    write_json_str(f, field.canonical_type)?;
86    write!(
87        f,
88        ", \"size\": {}, \"offset\": {}",
89        field.size, field.offset
90    )?;
91    write!(f, ", \"intent\": ")?;
92    write_json_str(f, field.intent.name())?;
93    write!(f, " }}")
94}
95
96fn write_fields_json(
97    f: &mut fmt::Formatter<'_>,
98    fields: &[FieldDescriptor],
99    indent: usize,
100) -> fmt::Result {
101    if fields.is_empty() {
102        return write!(f, "[]");
103    }
104    writeln!(f, "[")?;
105    for (i, field) in fields.iter().enumerate() {
106        write_field_json(f, field, indent + 1)?;
107        if i + 1 < fields.len() {
108            writeln!(f, ",")?;
109        } else {
110            writeln!(f)?;
111        }
112    }
113    write_indent(f, indent)?;
114    write!(f, "]")
115}
116
117// ---------------------------------------------------------------------------
118// ArgDescriptor JSON
119// ---------------------------------------------------------------------------
120
121fn write_args_json(
122    f: &mut fmt::Formatter<'_>,
123    args: &[ArgDescriptor],
124    indent: usize,
125) -> fmt::Result {
126    if args.is_empty() {
127        return write!(f, "[]");
128    }
129    writeln!(f, "[")?;
130    for (i, arg) in args.iter().enumerate() {
131        write_indent(f, indent + 1)?;
132        write!(f, "{{ \"name\": ")?;
133        write_json_str(f, arg.name)?;
134        write!(f, ", \"type\": ")?;
135        write_json_str(f, arg.canonical_type)?;
136        write!(f, ", \"size\": {} }}", arg.size)?;
137        if i + 1 < args.len() {
138            writeln!(f, ",")?;
139        } else {
140            writeln!(f)?;
141        }
142    }
143    write_indent(f, indent)?;
144    write!(f, "]")
145}
146
147// ---------------------------------------------------------------------------
148// CodamaProjection JSON
149// ---------------------------------------------------------------------------
150
151fn write_idl_account_json(
152    f: &mut fmt::Formatter<'_>,
153    a: &IdlAccountEntry,
154    indent: usize,
155) -> fmt::Result {
156    write_indent(f, indent)?;
157    writeln!(f, "{{")?;
158    write_indent(f, indent + 1)?;
159    write!(f, "\"name\": ")?;
160    write_json_str(f, a.name)?;
161    writeln!(f, ",")?;
162    write_indent(f, indent + 1)?;
163    writeln!(f, "\"writable\": {},", a.writable)?;
164    write_indent(f, indent + 1)?;
165    write!(f, "\"signer\": {}", a.signer)?;
166    if !a.layout_ref.is_empty() {
167        writeln!(f, ",")?;
168        write_indent(f, indent + 1)?;
169        write!(f, "\"layoutRef\": ")?;
170        write_json_str(f, a.layout_ref)?;
171    }
172    if !a.pda_seeds.is_empty() {
173        writeln!(f, ",")?;
174        write_indent(f, indent + 1)?;
175        write!(f, "\"pdaSeeds\": ")?;
176        write_pda_seeds_json(f, a.pda_seeds, indent + 1)?;
177    }
178    writeln!(f)?;
179    write_indent(f, indent)?;
180    write!(f, "}}")
181}
182
183fn write_pda_seeds_json(
184    f: &mut fmt::Formatter<'_>,
185    seeds: &[PdaSeedHint],
186    indent: usize,
187) -> fmt::Result {
188    writeln!(f, "[")?;
189    for (i, seed) in seeds.iter().enumerate() {
190        write_indent(f, indent + 1)?;
191        write!(f, "{{ \"kind\": ")?;
192        write_json_str(f, seed.kind)?;
193        write!(f, ", \"value\": ")?;
194        write_json_str(f, seed.value)?;
195        write!(f, " }}")?;
196        if i + 1 < seeds.len() {
197            writeln!(f, ",")?;
198        } else {
199            writeln!(f)?;
200        }
201    }
202    write_indent(f, indent)?;
203    write!(f, "]")
204}
205
206/// Wrapper for JSON formatting of `CodamaProjection`.
207pub struct CodamaJson<'a>(pub &'a CodamaProjection);
208
209impl<'a> fmt::Display for CodamaJson<'a> {
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        let p = self.0;
212        writeln!(f, "{{")?;
213        write!(f, "  \"name\": ")?;
214        write_json_str(f, p.name)?;
215        writeln!(f, ",")?;
216        write!(f, "  \"version\": ")?;
217        write_json_str(f, p.version)?;
218        writeln!(f, ",")?;
219
220        // Instructions
221        write!(f, "  \"instructions\": ")?;
222        if p.instructions.is_empty() {
223            writeln!(f, "[],")?;
224        } else {
225            writeln!(f, "[")?;
226            for (i, ix) in p.instructions.iter().enumerate() {
227                write_indent(f, 2)?;
228                writeln!(f, "{{")?;
229                write_indent(f, 3)?;
230                write!(f, "\"name\": ")?;
231                write_json_str(f, ix.name)?;
232                writeln!(f, ",")?;
233                write_indent(f, 3)?;
234                writeln!(f, "\"discriminator\": {},", ix.discriminator)?;
235                write_indent(f, 3)?;
236                write!(f, "\"args\": ")?;
237                write_args_json(f, ix.args, 3)?;
238                writeln!(f, ",")?;
239                write_indent(f, 3)?;
240                write!(f, "\"accounts\": ")?;
241                if ix.accounts.is_empty() {
242                    write!(f, "[]")?;
243                } else {
244                    writeln!(f, "[")?;
245                    for (j, a) in ix.accounts.iter().enumerate() {
246                        write_idl_account_json(f, a, 4)?;
247                        if j + 1 < ix.accounts.len() {
248                            writeln!(f, ",")?;
249                        } else {
250                            writeln!(f)?;
251                        }
252                    }
253                    write_indent(f, 3)?;
254                    write!(f, "]")?;
255                }
256                writeln!(f)?;
257                write_indent(f, 2)?;
258                write!(f, "}}")?;
259                if i + 1 < p.instructions.len() {
260                    writeln!(f, ",")?;
261                } else {
262                    writeln!(f)?;
263                }
264            }
265            writeln!(f, "  ],")?;
266        }
267
268        // Accounts
269        write!(f, "  \"accounts\": ")?;
270        if p.accounts.is_empty() {
271            writeln!(f, "[],")?;
272        } else {
273            writeln!(f, "[")?;
274            for (i, a) in p.accounts.iter().enumerate() {
275                write_indent(f, 2)?;
276                writeln!(f, "{{")?;
277                write_indent(f, 3)?;
278                write!(f, "\"name\": ")?;
279                write_json_str(f, a.name)?;
280                writeln!(f, ",")?;
281                write_indent(f, 3)?;
282                writeln!(f, "\"discriminator\": {},", a.discriminator)?;
283                write_indent(f, 3)?;
284                writeln!(f, "\"size\": {},", a.size)?;
285                write_indent(f, 3)?;
286                write!(f, "\"fields\": ")?;
287                write_fields_json(f, a.fields, 3)?;
288                writeln!(f)?;
289                write_indent(f, 2)?;
290                write!(f, "}}")?;
291                if i + 1 < p.accounts.len() {
292                    writeln!(f, ",")?;
293                } else {
294                    writeln!(f)?;
295                }
296            }
297            writeln!(f, "  ],")?;
298        }
299
300        // Events
301        write!(f, "  \"events\": ")?;
302        if p.events.is_empty() {
303            writeln!(f, "[]")?;
304        } else {
305            writeln!(f, "[")?;
306            for (i, e) in p.events.iter().enumerate() {
307                write_indent(f, 2)?;
308                writeln!(f, "{{")?;
309                write_indent(f, 3)?;
310                write!(f, "\"name\": ")?;
311                write_json_str(f, e.name)?;
312                writeln!(f, ",")?;
313                write_indent(f, 3)?;
314                writeln!(f, "\"discriminator\": {},", e.discriminator)?;
315                write_indent(f, 3)?;
316                write!(f, "\"fields\": ")?;
317                write_fields_json(f, e.fields, 3)?;
318                writeln!(f)?;
319                write_indent(f, 2)?;
320                write!(f, "}}")?;
321                if i + 1 < p.events.len() {
322                    writeln!(f, ",")?;
323                } else {
324                    writeln!(f)?;
325                }
326            }
327            writeln!(f, "  ]")?;
328        }
329
330        write!(f, "}}")
331    }
332}
333
334// ---------------------------------------------------------------------------
335// ProgramIdl JSON
336// ---------------------------------------------------------------------------
337
338/// Wrapper for JSON formatting of `ProgramIdl`.
339pub struct IdlJson<'a>(pub &'a ProgramIdl);
340
341impl<'a> fmt::Display for IdlJson<'a> {
342    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
343        let p = self.0;
344        writeln!(f, "{{")?;
345        write!(f, "  \"name\": ")?;
346        write_json_str(f, p.name)?;
347        writeln!(f, ",")?;
348        write!(f, "  \"version\": ")?;
349        write_json_str(f, p.version)?;
350        writeln!(f, ",")?;
351        write!(f, "  \"description\": ")?;
352        write_json_str(f, p.description)?;
353        writeln!(f, ",")?;
354
355        // Instructions
356        write!(f, "  \"instructions\": ")?;
357        if p.instructions.is_empty() {
358            writeln!(f, "[],")?;
359        } else {
360            writeln!(f, "[")?;
361            for (i, ix) in p.instructions.iter().enumerate() {
362                write_indent(f, 2)?;
363                writeln!(f, "{{")?;
364                write_indent(f, 3)?;
365                write!(f, "\"name\": ")?;
366                write_json_str(f, ix.name)?;
367                writeln!(f, ",")?;
368                write_indent(f, 3)?;
369                writeln!(f, "\"tag\": {},", ix.tag)?;
370                write_indent(f, 3)?;
371                write!(f, "\"args\": ")?;
372                write_args_json(f, ix.args, 3)?;
373                writeln!(f, ",")?;
374                write_indent(f, 3)?;
375                write!(f, "\"accounts\": ")?;
376                if ix.accounts.is_empty() {
377                    write!(f, "[]")?;
378                } else {
379                    writeln!(f, "[")?;
380                    for (j, a) in ix.accounts.iter().enumerate() {
381                        write_idl_account_json(f, a, 4)?;
382                        if j + 1 < ix.accounts.len() {
383                            writeln!(f, ",")?;
384                        } else {
385                            writeln!(f)?;
386                        }
387                    }
388                    write_indent(f, 3)?;
389                    write!(f, "]")?;
390                }
391                writeln!(f)?;
392                write_indent(f, 2)?;
393                write!(f, "}}")?;
394                if i + 1 < p.instructions.len() {
395                    writeln!(f, ",")?;
396                } else {
397                    writeln!(f)?;
398                }
399            }
400            writeln!(f, "  ],")?;
401        }
402
403        // Accounts
404        write!(f, "  \"accounts\": ")?;
405        if p.accounts.is_empty() {
406            writeln!(f, "[],")?;
407        } else {
408            writeln!(f, "[")?;
409            for (i, a) in p.accounts.iter().enumerate() {
410                write_indent(f, 2)?;
411                writeln!(f, "{{")?;
412                write_indent(f, 3)?;
413                write!(f, "\"name\": ")?;
414                write_json_str(f, a.name)?;
415                writeln!(f, ",")?;
416                write_indent(f, 3)?;
417                writeln!(f, "\"disc\": {},", a.disc)?;
418                write_indent(f, 3)?;
419                writeln!(f, "\"version\": {},", a.version)?;
420                write_indent(f, 3)?;
421                write!(f, "\"layoutId\": ")?;
422                write_hex_json(f, &a.layout_id)?;
423                writeln!(f, ",")?;
424                write_indent(f, 3)?;
425                writeln!(f, "\"totalSize\": {},", a.total_size)?;
426                write_indent(f, 3)?;
427                write!(f, "\"fields\": ")?;
428                write_fields_json(f, a.fields, 3)?;
429                writeln!(f)?;
430                write_indent(f, 2)?;
431                write!(f, "}}")?;
432                if i + 1 < p.accounts.len() {
433                    writeln!(f, ",")?;
434                } else {
435                    writeln!(f)?;
436                }
437            }
438            writeln!(f, "  ],")?;
439        }
440
441        // Events
442        write!(f, "  \"events\": ")?;
443        if p.events.is_empty() {
444            writeln!(f, "[],")?;
445        } else {
446            writeln!(f, "[")?;
447            for (i, e) in p.events.iter().enumerate() {
448                write_indent(f, 2)?;
449                writeln!(f, "{{")?;
450                write_indent(f, 3)?;
451                write!(f, "\"name\": ")?;
452                write_json_str(f, e.name)?;
453                writeln!(f, ",")?;
454                write_indent(f, 3)?;
455                writeln!(f, "\"tag\": {},", e.tag)?;
456                write_indent(f, 3)?;
457                write!(f, "\"fields\": ")?;
458                write_fields_json(f, e.fields, 3)?;
459                writeln!(f)?;
460                write_indent(f, 2)?;
461                write!(f, "}}")?;
462                if i + 1 < p.events.len() {
463                    writeln!(f, ",")?;
464                } else {
465                    writeln!(f)?;
466                }
467            }
468            writeln!(f, "  ],")?;
469        }
470
471        // Fingerprints
472        write!(f, "  \"fingerprints\": ")?;
473        if p.fingerprints.is_empty() {
474            writeln!(f, "[]")?;
475        } else {
476            writeln!(f, "[")?;
477            for (i, (fp, name)) in p.fingerprints.iter().enumerate() {
478                write_indent(f, 2)?;
479                write!(f, "{{ \"layoutId\": ")?;
480                write_hex_json(f, fp)?;
481                write!(f, ", \"name\": ")?;
482                write_json_str(f, name)?;
483                write!(f, " }}")?;
484                if i + 1 < p.fingerprints.len() {
485                    writeln!(f, ",")?;
486                } else {
487                    writeln!(f)?;
488                }
489            }
490            writeln!(f, "  ]")?;
491        }
492
493        write!(f, "}}")
494    }
495}
496
497// ---------------------------------------------------------------------------
498// ProgramManifest JSON
499// ---------------------------------------------------------------------------
500
501/// Wrapper for JSON formatting of `ProgramManifest`.
502pub struct ManifestJson<'a>(pub &'a ProgramManifest);
503
504impl<'a> fmt::Display for ManifestJson<'a> {
505    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
506        let p = self.0;
507        writeln!(f, "{{")?;
508        write!(f, "  \"name\": ")?;
509        write_json_str(f, p.name)?;
510        writeln!(f, ",")?;
511        write!(f, "  \"version\": ")?;
512        write_json_str(f, p.version)?;
513        writeln!(f, ",")?;
514        write!(f, "  \"description\": ")?;
515        write_json_str(f, p.description)?;
516        writeln!(f, ",")?;
517
518        // Layouts
519        write!(f, "  \"layouts\": ")?;
520        write_layout_array(f, p.layouts)?;
521        writeln!(f, ",")?;
522
523        // Instructions
524        write!(f, "  \"instructions\": ")?;
525        write_instruction_array(f, p.instructions)?;
526        writeln!(f, ",")?;
527
528        // Events
529        write!(f, "  \"events\": ")?;
530        write_event_array(f, p.events)?;
531        writeln!(f, ",")?;
532
533        // Policies
534        write!(f, "  \"policies\": ")?;
535        write_policy_array(f, p.policies)?;
536        writeln!(f, ",")?;
537
538        // Compatibility rules
539        write!(f, "  \"compatRules\": ")?;
540        write_compat_pair_array(f, p.compatibility_pairs)?;
541        writeln!(f, ",")?;
542
543        // Receipt wire schema
544        write!(f, "  \"receiptSchema\": ")?;
545        write_receipt_schema(f)?;
546        writeln!(f, ",")?;
547
548        // Tooling hints
549        write!(f, "  \"toolingHints\": ")?;
550        write_str_array(f, p.tooling_hints, 1)?;
551        writeln!(f)?;
552
553        write!(f, "}}")
554    }
555}
556
557fn write_layout_array(f: &mut fmt::Formatter<'_>, layouts: &[LayoutManifest]) -> fmt::Result {
558    if layouts.is_empty() {
559        return write!(f, "[]");
560    }
561    writeln!(f, "[")?;
562    for (i, l) in layouts.iter().enumerate() {
563        write_indent(f, 2)?;
564        writeln!(f, "{{")?;
565        write_indent(f, 3)?;
566        write!(f, "\"name\": ")?;
567        write_json_str(f, l.name)?;
568        writeln!(f, ",")?;
569        write_indent(f, 3)?;
570        writeln!(f, "\"disc\": {},", l.disc)?;
571        write_indent(f, 3)?;
572        writeln!(f, "\"version\": {},", l.version)?;
573        write_indent(f, 3)?;
574        write!(f, "\"layoutId\": ")?;
575        write_hex_json(f, &l.layout_id)?;
576        writeln!(f, ",")?;
577        write_indent(f, 3)?;
578        writeln!(f, "\"totalSize\": {},", l.total_size)?;
579        write_indent(f, 3)?;
580        writeln!(f, "\"fieldCount\": {},", l.field_count)?;
581        write_indent(f, 3)?;
582        write!(f, "\"fields\": ")?;
583        write_fields_json(f, l.fields, 3)?;
584        writeln!(f, ",")?;
585        // Semantic fingerprint (v2)
586        let fp = LayoutFingerprint::from_manifest(l);
587        write_indent(f, 3)?;
588        write!(f, "\"semanticFingerprint\": ")?;
589        write_hex_json(f, &fp.semantic_hash)?;
590        writeln!(f)?;
591        write_indent(f, 2)?;
592        write!(f, "}}")?;
593        if i + 1 < layouts.len() {
594            writeln!(f, ",")?;
595        } else {
596            writeln!(f)?;
597        }
598    }
599    write_indent(f, 1)?;
600    write!(f, "]")
601}
602
603fn write_instruction_array(
604    f: &mut fmt::Formatter<'_>,
605    instrs: &[InstructionDescriptor],
606) -> fmt::Result {
607    if instrs.is_empty() {
608        return write!(f, "[]");
609    }
610    writeln!(f, "[")?;
611    for (i, ix) in instrs.iter().enumerate() {
612        write_indent(f, 2)?;
613        writeln!(f, "{{")?;
614        write_indent(f, 3)?;
615        write!(f, "\"name\": ")?;
616        write_json_str(f, ix.name)?;
617        writeln!(f, ",")?;
618        write_indent(f, 3)?;
619        writeln!(f, "\"tag\": {},", ix.tag)?;
620        write_indent(f, 3)?;
621        write!(f, "\"args\": ")?;
622        write_args_json(f, ix.args, 3)?;
623        writeln!(f, ",")?;
624        write_indent(f, 3)?;
625        write!(f, "\"accounts\": ")?;
626        write_account_entry_array(f, ix.accounts, 3)?;
627        writeln!(f, ",")?;
628        write_indent(f, 3)?;
629        write!(f, "\"capabilities\": ")?;
630        write_str_array(f, ix.capabilities, 3)?;
631        writeln!(f, ",")?;
632        write_indent(f, 3)?;
633        write!(f, "\"policyPack\": ")?;
634        write_json_str(f, ix.policy_pack)?;
635        writeln!(f, ",")?;
636        write_indent(f, 3)?;
637        writeln!(f, "\"receiptExpected\": {}", ix.receipt_expected)?;
638        write_indent(f, 2)?;
639        write!(f, "}}")?;
640        if i + 1 < instrs.len() {
641            writeln!(f, ",")?;
642        } else {
643            writeln!(f)?;
644        }
645    }
646    write_indent(f, 1)?;
647    write!(f, "]")
648}
649
650fn write_account_entry_array(
651    f: &mut fmt::Formatter<'_>,
652    accounts: &[crate::AccountEntry],
653    indent: usize,
654) -> fmt::Result {
655    if accounts.is_empty() {
656        return write!(f, "[]");
657    }
658    writeln!(f, "[")?;
659    for (i, a) in accounts.iter().enumerate() {
660        write_indent(f, indent + 1)?;
661        write!(f, "{{ \"name\": ")?;
662        write_json_str(f, a.name)?;
663        write!(
664            f,
665            ", \"writable\": {}, \"signer\": {}",
666            a.writable, a.signer
667        )?;
668        if !a.layout_ref.is_empty() {
669            write!(f, ", \"layoutRef\": ")?;
670            write_json_str(f, a.layout_ref)?;
671        }
672        write!(f, " }}")?;
673        if i + 1 < accounts.len() {
674            writeln!(f, ",")?;
675        } else {
676            writeln!(f)?;
677        }
678    }
679    write_indent(f, indent)?;
680    write!(f, "]")
681}
682
683fn write_event_array(f: &mut fmt::Formatter<'_>, events: &[EventDescriptor]) -> fmt::Result {
684    if events.is_empty() {
685        return write!(f, "[]");
686    }
687    writeln!(f, "[")?;
688    for (i, e) in events.iter().enumerate() {
689        write_indent(f, 2)?;
690        writeln!(f, "{{")?;
691        write_indent(f, 3)?;
692        write!(f, "\"name\": ")?;
693        write_json_str(f, e.name)?;
694        writeln!(f, ",")?;
695        write_indent(f, 3)?;
696        writeln!(f, "\"tag\": {},", e.tag)?;
697        write_indent(f, 3)?;
698        write!(f, "\"fields\": ")?;
699        write_fields_json(f, e.fields, 3)?;
700        writeln!(f)?;
701        write_indent(f, 2)?;
702        write!(f, "}}")?;
703        if i + 1 < events.len() {
704            writeln!(f, ",")?;
705        } else {
706            writeln!(f)?;
707        }
708    }
709    write_indent(f, 1)?;
710    write!(f, "]")
711}
712
713fn write_policy_array(f: &mut fmt::Formatter<'_>, policies: &[PolicyDescriptor]) -> fmt::Result {
714    if policies.is_empty() {
715        return write!(f, "[]");
716    }
717    writeln!(f, "[")?;
718    for (i, p) in policies.iter().enumerate() {
719        write_indent(f, 2)?;
720        writeln!(f, "{{")?;
721        write_indent(f, 3)?;
722        write!(f, "\"name\": ")?;
723        write_json_str(f, p.name)?;
724        writeln!(f, ",")?;
725        write_indent(f, 3)?;
726        write!(f, "\"capabilities\": ")?;
727        write_str_array(f, p.capabilities, 3)?;
728        writeln!(f, ",")?;
729        write_indent(f, 3)?;
730        write!(f, "\"requirements\": ")?;
731        write_str_array(f, p.requirements, 3)?;
732        writeln!(f, ",")?;
733        write_indent(f, 3)?;
734        write!(f, "\"invariants\": ")?;
735        write_str_array(f, p.invariants, 3)?;
736        writeln!(f, ",")?;
737        write_indent(f, 3)?;
738        write!(f, "\"receiptProfile\": ")?;
739        write_json_str(f, p.receipt_profile)?;
740        writeln!(f)?;
741        write_indent(f, 2)?;
742        write!(f, "}}")?;
743        if i + 1 < policies.len() {
744            writeln!(f, ",")?;
745        } else {
746            writeln!(f)?;
747        }
748    }
749    write_indent(f, 1)?;
750    write!(f, "]")
751}
752
753fn write_compat_pair_array(f: &mut fmt::Formatter<'_>, pairs: &[CompatibilityPair]) -> fmt::Result {
754    if pairs.is_empty() {
755        return write!(f, "[]");
756    }
757    writeln!(f, "[")?;
758    for (i, cp) in pairs.iter().enumerate() {
759        write_indent(f, 2)?;
760        writeln!(f, "{{")?;
761        write_indent(f, 3)?;
762        write!(f, "\"from\": ")?;
763        write_json_str(f, cp.from_layout)?;
764        writeln!(f, ",")?;
765        write_indent(f, 3)?;
766        writeln!(f, "\"fromVersion\": {},", cp.from_version)?;
767        write_indent(f, 3)?;
768        write!(f, "\"to\": ")?;
769        write_json_str(f, cp.to_layout)?;
770        writeln!(f, ",")?;
771        write_indent(f, 3)?;
772        writeln!(f, "\"toVersion\": {},", cp.to_version)?;
773        write_indent(f, 3)?;
774        let policy_name = match cp.policy {
775            MigrationPolicy::NoOp => "noop",
776            MigrationPolicy::AppendOnly => "append-only",
777            MigrationPolicy::RequiresMigration => "requires-migration",
778            MigrationPolicy::Incompatible => "incompatible",
779        };
780        write!(f, "\"policy\": ")?;
781        write_json_str(f, policy_name)?;
782        writeln!(f, ",")?;
783        write_indent(f, 3)?;
784        writeln!(f, "\"backwardReadable\": {}", cp.backward_readable)?;
785        write_indent(f, 2)?;
786        write!(f, "}}")?;
787        if i + 1 < pairs.len() {
788            writeln!(f, ",")?;
789        } else {
790            writeln!(f)?;
791        }
792    }
793    write_indent(f, 1)?;
794    write!(f, "]")
795}
796
797/// Emit the fixed 64-byte receipt wire schema as a JSON object.
798/// This describes the binary layout so tools can decode receipts
799/// without linking the Hopper crate.
800fn write_receipt_schema(f: &mut fmt::Formatter<'_>) -> fmt::Result {
801    writeln!(f, "{{")?;
802    write_indent(f, 2)?;
803    writeln!(f, "\"size\": 64,")?;
804    write_indent(f, 2)?;
805    writeln!(f, "\"fields\": [")?;
806    let fields: &[(&str, &str, u8, u8)] = &[
807        ("layout_id", "bytes", 0, 8),
808        ("phase", "u8", 8, 1),
809        ("committed", "bool", 9, 1),
810        ("changed_fields", "u64", 10, 8),
811        ("changed_bytes", "u16", 18, 2),
812        ("changed_regions", "u8", 20, 1),
813        ("was_resized", "bool", 21, 1),
814        ("old_size", "u16", 22, 2),
815        ("new_size", "u16", 24, 2),
816        ("before_fingerprint", "bytes", 26, 4),
817        ("after_fingerprint", "bytes", 30, 4),
818        ("invariants_passed", "bool", 34, 1),
819        ("invariants_checked", "u8", 35, 1),
820        ("cpi_invoked", "bool", 36, 1),
821        ("cpi_count", "u8", 37, 1),
822        ("journal_appends", "u8", 38, 1),
823        ("segment_changed_mask", "u16", 39, 2),
824        ("policy_flags", "u32", 41, 4),
825        ("compat_impact", "u8", 45, 1),
826        ("validation_bundle_id", "u8", 46, 1),
827        ("migration_flags", "u8", 47, 1),
828    ];
829    for (i, (name, ty, offset, size)) in fields.iter().enumerate() {
830        write_indent(f, 3)?;
831        write!(f, "{{ \"name\": ")?;
832        write_json_str(f, name)?;
833        write!(f, ", \"type\": ")?;
834        write_json_str(f, ty)?;
835        write!(f, ", \"offset\": {}, \"size\": {} }}", offset, size)?;
836        if i + 1 < fields.len() {
837            writeln!(f, ",")?;
838        } else {
839            writeln!(f)?;
840        }
841    }
842    write_indent(f, 2)?;
843    writeln!(f, "]")?;
844    write_indent(f, 1)?;
845    write!(f, "}}")
846}
847
848// ---------------------------------------------------------------------------
849// Projection wrappers: ProgramManifest → IDL JSON / Codama JSON
850// ---------------------------------------------------------------------------
851
852/// Projects a `ProgramManifest` to IDL-level JSON (public schema subset).
853///
854/// Strips internal policy logic, migration planner hints, trust internals,
855/// and unsafe metadata. Retains: instructions (with args + accounts),
856/// account layouts (with fields), events, and layout fingerprints.
857pub struct IdlJsonFromManifest<'a>(pub &'a ProgramManifest);
858
859impl<'a> fmt::Display for IdlJsonFromManifest<'a> {
860    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
861        let p = self.0;
862        writeln!(f, "{{")?;
863        write!(f, "  \"name\": ")?;
864        write_json_str(f, p.name)?;
865        writeln!(f, ",")?;
866        write!(f, "  \"version\": ")?;
867        write_json_str(f, p.version)?;
868        writeln!(f, ",")?;
869        write!(f, "  \"description\": ")?;
870        write_json_str(f, p.description)?;
871        writeln!(f, ",")?;
872
873        // Instructions (projected: drop capabilities, policy_pack, receipt_expected)
874        write!(f, "  \"instructions\": ")?;
875        if p.instructions.is_empty() {
876            writeln!(f, "[],")?;
877        } else {
878            writeln!(f, "[")?;
879            for (i, ix) in p.instructions.iter().enumerate() {
880                write_indent(f, 2)?;
881                writeln!(f, "{{")?;
882                write_indent(f, 3)?;
883                write!(f, "\"name\": ")?;
884                write_json_str(f, ix.name)?;
885                writeln!(f, ",")?;
886                write_indent(f, 3)?;
887                writeln!(f, "\"tag\": {},", ix.tag)?;
888                write_indent(f, 3)?;
889                write!(f, "\"args\": ")?;
890                write_args_json(f, ix.args, 3)?;
891                writeln!(f, ",")?;
892                write_indent(f, 3)?;
893                write!(f, "\"accounts\": ")?;
894                write_account_entry_array(f, ix.accounts, 3)?;
895                writeln!(f)?;
896                write_indent(f, 2)?;
897                write!(f, "}}")?;
898                if i + 1 < p.instructions.len() {
899                    writeln!(f, ",")?;
900                } else {
901                    writeln!(f)?;
902                }
903            }
904            writeln!(f, "  ],")?;
905        }
906
907        // Accounts (full layout manifests)
908        write!(f, "  \"accounts\": ")?;
909        write_layout_array(f, p.layouts)?;
910        writeln!(f, ",")?;
911
912        // Events
913        write!(f, "  \"events\": ")?;
914        write_event_array(f, p.events)?;
915        writeln!(f, ",")?;
916
917        // Fingerprints (derived from layouts)
918        write!(f, "  \"fingerprints\": ")?;
919        if p.layouts.is_empty() {
920            writeln!(f, "[]")?;
921        } else {
922            writeln!(f, "[")?;
923            for (i, l) in p.layouts.iter().enumerate() {
924                write_indent(f, 2)?;
925                write!(f, "{{ \"layoutId\": ")?;
926                write_hex_json(f, &l.layout_id)?;
927                write!(f, ", \"name\": ")?;
928                write_json_str(f, l.name)?;
929                write!(f, " }}")?;
930                if i + 1 < p.layouts.len() {
931                    writeln!(f, ",")?;
932                } else {
933                    writeln!(f)?;
934                }
935            }
936            writeln!(f, "  ]")?;
937        }
938
939        write!(f, "}}")
940    }
941}
942
943/// Projects a `ProgramManifest` to Codama-level JSON (interop subset).
944///
945/// Only the minimal fields needed for Codama/Kinobi tooling:
946/// instructions (name, discriminator, args, accounts), account types
947/// (name, discriminator, size, fields), and events.
948pub struct CodamaJsonFromManifest<'a>(pub &'a ProgramManifest);
949
950impl<'a> fmt::Display for CodamaJsonFromManifest<'a> {
951    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
952        let p = self.0;
953        writeln!(f, "{{")?;
954        write!(f, "  \"name\": ")?;
955        write_json_str(f, p.name)?;
956        writeln!(f, ",")?;
957        write!(f, "  \"version\": ")?;
958        write_json_str(f, p.version)?;
959        writeln!(f, ",")?;
960
961        // Instructions (Codama: name, discriminator=tag, args, accounts as flat entries)
962        write!(f, "  \"instructions\": ")?;
963        if p.instructions.is_empty() {
964            writeln!(f, "[],")?;
965        } else {
966            writeln!(f, "[")?;
967            for (i, ix) in p.instructions.iter().enumerate() {
968                write_indent(f, 2)?;
969                writeln!(f, "{{")?;
970                write_indent(f, 3)?;
971                write!(f, "\"name\": ")?;
972                write_json_str(f, ix.name)?;
973                writeln!(f, ",")?;
974                write_indent(f, 3)?;
975                writeln!(f, "\"discriminator\": {},", ix.tag)?;
976                write_indent(f, 3)?;
977                write!(f, "\"args\": ")?;
978                write_args_json(f, ix.args, 3)?;
979                writeln!(f, ",")?;
980                write_indent(f, 3)?;
981                write!(f, "\"accounts\": ")?;
982                write_account_entry_array(f, ix.accounts, 3)?;
983                writeln!(f)?;
984                write_indent(f, 2)?;
985                write!(f, "}}")?;
986                if i + 1 < p.instructions.len() {
987                    writeln!(f, ",")?;
988                } else {
989                    writeln!(f)?;
990                }
991            }
992            writeln!(f, "  ],")?;
993        }
994
995        // Accounts (Codama: name, discriminator=disc, size, fields)
996        write!(f, "  \"accounts\": ")?;
997        if p.layouts.is_empty() {
998            writeln!(f, "[],")?;
999        } else {
1000            writeln!(f, "[")?;
1001            for (i, l) in p.layouts.iter().enumerate() {
1002                write_indent(f, 2)?;
1003                writeln!(f, "{{")?;
1004                write_indent(f, 3)?;
1005                write!(f, "\"name\": ")?;
1006                write_json_str(f, l.name)?;
1007                writeln!(f, ",")?;
1008                write_indent(f, 3)?;
1009                writeln!(f, "\"discriminator\": {},", l.disc)?;
1010                write_indent(f, 3)?;
1011                writeln!(f, "\"size\": {},", l.total_size)?;
1012                write_indent(f, 3)?;
1013                write!(f, "\"fields\": ")?;
1014                write_fields_json(f, l.fields, 3)?;
1015                writeln!(f)?;
1016                write_indent(f, 2)?;
1017                write!(f, "}}")?;
1018                if i + 1 < p.layouts.len() {
1019                    writeln!(f, ",")?;
1020                } else {
1021                    writeln!(f)?;
1022                }
1023            }
1024            writeln!(f, "  ],")?;
1025        }
1026
1027        // Events (Codama: name, discriminator=tag, fields)
1028        write!(f, "  \"events\": ")?;
1029        if p.events.is_empty() {
1030            writeln!(f, "[]")?;
1031        } else {
1032            writeln!(f, "[")?;
1033            for (i, e) in p.events.iter().enumerate() {
1034                write_indent(f, 2)?;
1035                writeln!(f, "{{")?;
1036                write_indent(f, 3)?;
1037                write!(f, "\"name\": ")?;
1038                write_json_str(f, e.name)?;
1039                writeln!(f, ",")?;
1040                write_indent(f, 3)?;
1041                writeln!(f, "\"discriminator\": {},", e.tag)?;
1042                write_indent(f, 3)?;
1043                write!(f, "\"fields\": ")?;
1044                write_fields_json(f, e.fields, 3)?;
1045                writeln!(f)?;
1046                write_indent(f, 2)?;
1047                write!(f, "}}")?;
1048                if i + 1 < p.events.len() {
1049                    writeln!(f, ",")?;
1050                } else {
1051                    writeln!(f)?;
1052                }
1053            }
1054            writeln!(f, "  ]")?;
1055        }
1056
1057        write!(f, "}}")
1058    }
1059}
1060
1061// ---------------------------------------------------------------------------
1062// Tests
1063// ---------------------------------------------------------------------------
1064
1065#[cfg(test)]
1066mod tests {
1067    extern crate alloc;
1068    use alloc::format;
1069
1070    use super::*;
1071    use crate::{
1072        CodamaAccount, CodamaInstruction, CodamaProjection, FieldIntent, ProgramIdl,
1073        ProgramManifest,
1074    };
1075
1076    #[test]
1077    fn codama_json_empty() {
1078        let c = CodamaProjection::empty();
1079        let json = format!("{}", CodamaJson(&c));
1080        assert!(json.contains("\"name\": \"\""));
1081        assert!(json.contains("\"instructions\": []"));
1082        assert!(json.contains("\"accounts\": []"));
1083        assert!(json.contains("\"events\": []"));
1084    }
1085
1086    #[test]
1087    fn idl_json_empty() {
1088        let idl = ProgramIdl::empty();
1089        let json = format!("{}", IdlJson(&idl));
1090        assert!(json.contains("\"name\": \"\""));
1091        assert!(json.contains("\"instructions\": []"));
1092        assert!(json.contains("\"accounts\": []"));
1093        assert!(json.contains("\"events\": []"));
1094        assert!(json.contains("\"fingerprints\": []"));
1095    }
1096
1097    #[test]
1098    fn manifest_json_empty() {
1099        let m = ProgramManifest::empty();
1100        let json = format!("{}", ManifestJson(&m));
1101        assert!(json.contains("\"name\": \"\""));
1102        assert!(json.contains("\"layouts\": []"));
1103        assert!(json.contains("\"instructions\": []"));
1104        assert!(json.contains("\"events\": []"));
1105        assert!(json.contains("\"policies\": []"));
1106    }
1107
1108    #[test]
1109    fn codama_json_with_instruction() {
1110        static ARGS: &[ArgDescriptor] = &[ArgDescriptor {
1111            name: "amount",
1112            canonical_type: "WireU64",
1113            size: 8,
1114        }];
1115        static ACCOUNTS: &[IdlAccountEntry] = &[IdlAccountEntry {
1116            name: "vault",
1117            writable: true,
1118            signer: false,
1119            layout_ref: "VaultState",
1120            pda_seeds: &[],
1121        }];
1122        static IX: &[CodamaInstruction] = &[CodamaInstruction {
1123            name: "deposit",
1124            discriminator: 1,
1125            args: ARGS,
1126            accounts: ACCOUNTS,
1127        }];
1128        let c = CodamaProjection {
1129            name: "test_program",
1130            version: "0.1.0",
1131            instructions: IX,
1132            accounts: &[],
1133            events: &[],
1134        };
1135        let json = format!("{}", CodamaJson(&c));
1136        assert!(json.contains("\"test_program\""));
1137        assert!(json.contains("\"deposit\""));
1138        assert!(json.contains("\"discriminator\": 1"));
1139        assert!(json.contains("\"amount\""));
1140        assert!(json.contains("\"vault\""));
1141        assert!(json.contains("\"writable\": true"));
1142        assert!(json.contains("\"layoutRef\": \"VaultState\""));
1143    }
1144
1145    #[test]
1146    fn codama_json_with_account() {
1147        static FIELDS: &[FieldDescriptor] = &[FieldDescriptor {
1148            name: "balance",
1149            canonical_type: "WireU64",
1150            size: 8,
1151            offset: 16,
1152            intent: FieldIntent::Custom,
1153        }];
1154        static ACCTS: &[CodamaAccount] = &[CodamaAccount {
1155            name: "VaultState",
1156            discriminator: 1,
1157            size: 24,
1158            fields: FIELDS,
1159        }];
1160        let c = CodamaProjection {
1161            name: "test",
1162            version: "0.1.0",
1163            instructions: &[],
1164            accounts: ACCTS,
1165            events: &[],
1166        };
1167        let json = format!("{}", CodamaJson(&c));
1168        assert!(json.contains("\"VaultState\""));
1169        assert!(json.contains("\"discriminator\": 1"));
1170        assert!(json.contains("\"size\": 24"));
1171        assert!(json.contains("\"balance\""));
1172    }
1173
1174    #[test]
1175    fn manifest_json_with_policy() {
1176        static POLICIES: &[PolicyDescriptor] = &[PolicyDescriptor {
1177            name: "TREASURY_WRITE",
1178            capabilities: &["MutatesState", "MutatesTreasury"],
1179            requirements: &["SignerAuthority"],
1180            invariants: &[],
1181            receipt_profile: "full",
1182        }];
1183        let m = ProgramManifest {
1184            name: "test",
1185            version: "0.1.0",
1186            description: "A test program",
1187            layouts: &[],
1188            layout_metadata: &[],
1189            instructions: &[],
1190            events: &[],
1191            policies: POLICIES,
1192            compatibility_pairs: &[],
1193            tooling_hints: &["show_receipt"],
1194            contexts: &[],
1195        };
1196        let json = format!("{}", ManifestJson(&m));
1197        assert!(json.contains("\"TREASURY_WRITE\""));
1198        assert!(json.contains("\"MutatesState\""));
1199        assert!(json.contains("\"SignerAuthority\""));
1200        assert!(json.contains("\"full\""));
1201        assert!(json.contains("\"show_receipt\""));
1202    }
1203
1204    #[test]
1205    fn json_str_escapes_special_chars() {
1206        static FIELDS: &[FieldDescriptor] = &[];
1207        static ACCTS: &[CodamaAccount] = &[CodamaAccount {
1208            name: "has\"quotes",
1209            discriminator: 1,
1210            size: 16,
1211            fields: FIELDS,
1212        }];
1213        let c = CodamaProjection {
1214            name: "test\\prog",
1215            version: "1.0",
1216            instructions: &[],
1217            accounts: ACCTS,
1218            events: &[],
1219        };
1220        let json = format!("{}", CodamaJson(&c));
1221        assert!(json.contains("\"test\\\\prog\""));
1222        assert!(json.contains("\"has\\\"quotes\""));
1223    }
1224
1225    #[test]
1226    fn idl_from_manifest_projection() {
1227        static FIELDS: &[FieldDescriptor] = &[FieldDescriptor {
1228            name: "balance",
1229            canonical_type: "WireU64",
1230            size: 8,
1231            offset: 16,
1232            intent: FieldIntent::Custom,
1233        }];
1234        static LAYOUTS: &[LayoutManifest] = &[LayoutManifest {
1235            name: "Vault",
1236            disc: 1,
1237            version: 1,
1238            layout_id: [1, 2, 3, 4, 5, 6, 7, 8],
1239            total_size: 24,
1240            field_count: 1,
1241            fields: FIELDS,
1242        }];
1243        static ARGS: &[ArgDescriptor] = &[ArgDescriptor {
1244            name: "amount",
1245            canonical_type: "WireU64",
1246            size: 8,
1247        }];
1248        static ACCTS: &[crate::AccountEntry] = &[crate::AccountEntry {
1249            name: "vault",
1250            writable: true,
1251            signer: false,
1252            layout_ref: "Vault",
1253        }];
1254        static IX: &[InstructionDescriptor] = &[InstructionDescriptor {
1255            name: "deposit",
1256            tag: 1,
1257            args: ARGS,
1258            accounts: ACCTS,
1259            capabilities: &["MutatesState"],
1260            policy_pack: "TREASURY_WRITE",
1261            receipt_expected: true,
1262        }];
1263        let m = ProgramManifest {
1264            name: "vault_prog",
1265            version: "1.0.0",
1266            description: "A vault",
1267            layouts: LAYOUTS,
1268            layout_metadata: &[],
1269            instructions: IX,
1270            events: &[],
1271            policies: &[],
1272            compatibility_pairs: &[],
1273            tooling_hints: &[],
1274            contexts: &[],
1275        };
1276        let json = format!("{}", IdlJsonFromManifest(&m));
1277        // IDL should have instruction name+tag+args+accounts but NOT capabilities/policyPack
1278        assert!(json.contains("\"deposit\""));
1279        assert!(json.contains("\"tag\": 1"));
1280        assert!(json.contains("\"amount\""));
1281        assert!(json.contains("\"vault\""));
1282        assert!(!json.contains("\"capabilities\""));
1283        assert!(!json.contains("\"policyPack\""));
1284        assert!(!json.contains("\"receiptExpected\""));
1285        // IDL should have fingerprints derived from layouts
1286        assert!(json.contains("\"fingerprints\""));
1287        assert!(json.contains("\"Vault\""));
1288    }
1289
1290    #[test]
1291    fn codama_from_manifest_projection() {
1292        static ARGS: &[ArgDescriptor] = &[ArgDescriptor {
1293            name: "amount",
1294            canonical_type: "WireU64",
1295            size: 8,
1296        }];
1297        static ACCTS: &[crate::AccountEntry] = &[crate::AccountEntry {
1298            name: "vault",
1299            writable: true,
1300            signer: false,
1301            layout_ref: "Vault",
1302        }];
1303        static IX: &[InstructionDescriptor] = &[InstructionDescriptor {
1304            name: "deposit",
1305            tag: 1,
1306            args: ARGS,
1307            accounts: ACCTS,
1308            capabilities: &["MutatesState"],
1309            policy_pack: "TREASURY_WRITE",
1310            receipt_expected: true,
1311        }];
1312        static FIELDS: &[FieldDescriptor] = &[FieldDescriptor {
1313            name: "balance",
1314            canonical_type: "WireU64",
1315            size: 8,
1316            offset: 16,
1317            intent: FieldIntent::Custom,
1318        }];
1319        static LAYOUTS: &[LayoutManifest] = &[LayoutManifest {
1320            name: "Vault",
1321            disc: 1,
1322            version: 1,
1323            layout_id: [1, 2, 3, 4, 5, 6, 7, 8],
1324            total_size: 24,
1325            field_count: 1,
1326            fields: FIELDS,
1327        }];
1328        let m = ProgramManifest {
1329            name: "vault_prog",
1330            version: "1.0.0",
1331            description: "A vault",
1332            layouts: LAYOUTS,
1333            layout_metadata: &[],
1334            instructions: IX,
1335            events: &[],
1336            policies: &[],
1337            compatibility_pairs: &[],
1338            tooling_hints: &[],
1339            contexts: &[],
1340        };
1341        let json = format!("{}", CodamaJsonFromManifest(&m));
1342        // Codama: discriminator instead of tag
1343        assert!(json.contains("\"discriminator\": 1"));
1344        assert!(json.contains("\"deposit\""));
1345        assert!(json.contains("\"Vault\""));
1346        assert!(json.contains("\"size\": 24"));
1347        // Should NOT contain internal fields
1348        assert!(!json.contains("\"capabilities\""));
1349        assert!(!json.contains("\"policyPack\""));
1350        assert!(!json.contains("\"receiptExpected\""));
1351        assert!(!json.contains("\"fingerprints\""));
1352        assert!(!json.contains("\"toolingHints\""));
1353    }
1354}