Skip to main content

jiminy_schema/
lib.rs

1//! # jiminy-schema
2//!
3//! Layout Manifest v1 for Jiminy account schemas.
4//!
5//! This crate provides structured descriptions of Jiminy account layouts,
6//! enabling cross-language tooling, TypeScript decoder generation, indexer
7//! integration, and schema validation.
8//!
9//! ## Workflow
10//!
11//! ```text
12//! zero_copy_layout!  ──▶  LayoutManifest  ──▶  export_json()  ──▶  TS / indexers
13//! ```
14//!
15//! 1. `zero_copy_layout!` defines your on-chain struct and computes `LAYOUT_ID`.
16//! 2. Build a [`LayoutManifest`] describing the same struct.
17//! 3. [`export_json()`](LayoutManifest::export_json) emits a JSON manifest (no serde).
18//! 4. Off-chain tooling (`@jiminy/ts`, indexers, explorers) consumes the manifest.
19//!
20//! ## Layout Manifest v1
21//!
22//! A layout manifest describes one account type:
23//!
24//! ```rust
25//! use jiminy_schema::*;
26//!
27//! let manifest = LayoutManifest {
28//!     name: "Vault",
29//!     version: 1,
30//!     discriminator: 1,
31//!     layout_id: [0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67, 0x89],
32//!     fields: &[
33//!         FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
34//!         FieldDescriptor { name: "balance", canonical_type: CanonicalType::U64, size: 8 },
35//!         FieldDescriptor { name: "authority", canonical_type: CanonicalType::Pubkey, size: 32 },
36//!     ],
37//!     segments: &[],
38//! };
39//!
40//! assert_eq!(manifest.total_size(), 56);
41//! assert_eq!(manifest.field_offset("balance"), Some(16));
42//! ```
43//!
44//! ## Canonical Types
45//!
46//! The canonical type system normalizes Rust types to a fixed set of
47//! language-independent names. This matches the types used in
48//! `LAYOUT_ID` hash computation (see `LAYOUT_CONVENTION.md`).
49
50#![cfg_attr(not(feature = "std"), no_std)]
51
52/// Manifest format version string.
53///
54/// Included in every `export_json()` output. Tooling should check this
55/// value to detect incompatible manifest format changes. This const is
56/// frozen - it changes only with a major version bump.
57pub const MANIFEST_VERSION: &str = "manifest-v1";
58
59#[cfg(feature = "codegen")]
60pub mod codegen;
61
62#[cfg(feature = "std")]
63pub mod idl;
64
65#[cfg(feature = "std")]
66pub mod indexer;
67
68/// Canonical type identifiers for Jiminy account fields.
69///
70/// These correspond 1:1 to the canonical type strings used in
71/// `LAYOUT_ID` hash computation. Every Rust type maps to exactly
72/// one canonical type.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum CanonicalType {
75    /// `u8` — unsigned 8-bit integer.
76    U8,
77    /// `u16` — unsigned 16-bit integer (LE).
78    U16,
79    /// `u32` — unsigned 32-bit integer (LE).
80    U32,
81    /// `u64` — unsigned 64-bit integer (LE).
82    U64,
83    /// `u128` — unsigned 128-bit integer (LE).
84    U128,
85    /// `i8` — signed 8-bit integer.
86    I8,
87    /// `i16` — signed 16-bit integer (LE).
88    I16,
89    /// `i32` — signed 32-bit integer (LE).
90    I32,
91    /// `i64` — signed 64-bit integer (LE).
92    I64,
93    /// `i128` — signed 128-bit integer (LE).
94    I128,
95    /// `bool` — boolean (1 byte, 0 or 1).
96    Bool,
97    /// `pubkey` — 32-byte public key / address.
98    Pubkey,
99    /// `header` — Jiminy 16-byte `AccountHeader`.
100    Header,
101    /// Fixed-size byte array `[u8; N]`.
102    Bytes(usize),
103}
104
105impl CanonicalType {
106    /// Return the canonical string representation used in layout_id hashing.
107    pub const fn as_str(&self) -> &'static str {
108        match self {
109            Self::U8 => "u8",
110            Self::U16 => "u16",
111            Self::U32 => "u32",
112            Self::U64 => "u64",
113            Self::U128 => "u128",
114            Self::I8 => "i8",
115            Self::I16 => "i16",
116            Self::I32 => "i32",
117            Self::I64 => "i64",
118            Self::I128 => "i128",
119            Self::Bool => "bool",
120            Self::Pubkey => "pubkey",
121            Self::Header => "header",
122            Self::Bytes(_) => "bytes", // caller appends {N}
123        }
124    }
125}
126
127/// Describes a single field in a Jiminy account layout.
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub struct FieldDescriptor {
130    /// Field name (must match the Rust struct field name).
131    pub name: &'static str,
132    /// Canonical type of the field.
133    pub canonical_type: CanonicalType,
134    /// Size of the field in bytes.
135    pub size: usize,
136}
137
138/// Describes a dynamic segment in a segmented Jiminy account.
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub struct SegmentFieldDescriptor {
141    /// Segment name (e.g., `"bids"`).
142    pub name: &'static str,
143    /// Element type name (e.g., `"Order"`).
144    pub element_type: &'static str,
145    /// Size of each element in bytes.
146    pub element_size: usize,
147}
148
149/// A complete account layout manifest (v1).
150///
151/// Describes the schema of one Jiminy account type: its name, version,
152/// discriminator, layout_id, and ordered field list. This is the
153/// structured equivalent of the hash input string used to compute
154/// `LAYOUT_ID`.
155#[derive(Debug, Clone, PartialEq, Eq)]
156pub struct LayoutManifest {
157    /// Account type name (e.g., `"Vault"`).
158    pub name: &'static str,
159    /// Schema version.
160    pub version: u8,
161    /// Account discriminator byte.
162    pub discriminator: u8,
163    /// Deterministic layout_id (first 8 bytes of SHA-256).
164    pub layout_id: [u8; 8],
165    /// Ordered list of fields (including the header).
166    pub fields: &'static [FieldDescriptor],
167    /// Optional list of dynamic segments (for segmented layouts).
168    pub segments: &'static [SegmentFieldDescriptor],
169}
170
171impl LayoutManifest {
172    /// Total size of the account in bytes (sum of all field sizes).
173    pub const fn total_size(&self) -> usize {
174        let mut total = 0;
175        let mut i = 0;
176        while i < self.fields.len() {
177            total += self.fields[i].size;
178            i += 1;
179        }
180        total
181    }
182
183    /// Number of fields in the layout.
184    pub const fn field_count(&self) -> usize {
185        self.fields.len()
186    }
187
188    /// Find the byte offset of a field by name.
189    ///
190    /// Returns `None` if the field name is not found.
191    pub fn field_offset(&self, name: &str) -> Option<usize> {
192        let mut offset = 0;
193        for field in self.fields {
194            if field.name == name {
195                return Some(offset);
196            }
197            offset += field.size;
198        }
199        None
200    }
201
202    /// Find a field descriptor by name.
203    pub fn field(&self, name: &str) -> Option<&FieldDescriptor> {
204        self.fields.iter().find(|f| f.name == name)
205    }
206
207    /// Reconstruct the canonical hash input string.
208    ///
209    /// This produces the same string that `zero_copy_layout!` uses to
210    /// compute `LAYOUT_ID`, enabling verification that a manifest matches
211    /// a compiled layout.
212    #[cfg(feature = "std")]
213    pub fn hash_input(&self) -> String {
214        use std::fmt::Write;
215        let mut s = String::new();
216        write!(s, "jiminy:v1:{}:{}:", self.name, self.version).unwrap();
217        for field in self.fields {
218            match field.canonical_type {
219                CanonicalType::Bytes(n) => {
220                    write!(s, "{}:bytes{{{}}}:{},", field.name, n, field.size).unwrap();
221                }
222                _ => {
223                    write!(s, "{}:{}:{},", field.name, field.canonical_type.as_str(), field.size).unwrap();
224                }
225            }
226        }
227        for seg in self.segments {
228            write!(s, "seg:{}:{}:{},", seg.name, seg.element_type, seg.element_size).unwrap();
229        }
230        s
231    }
232
233    /// Export the manifest as a JSON string (no serde dependency).
234    ///
235    /// Produces a self-contained JSON document suitable for TypeScript
236    /// codegen, indexer ingestion, or cross-language tooling.
237    #[cfg(feature = "std")]
238    pub fn export_json(&self) -> String {
239        use std::fmt::Write;
240        let mut s = String::new();
241        s.push_str("{\n");
242        writeln!(s, "  \"version\": \"{}\",", MANIFEST_VERSION).unwrap();
243        writeln!(s, "  \"name\": \"{}\",", self.name).unwrap();
244        writeln!(s, "  \"schema_version\": {},", self.version).unwrap();
245        writeln!(s, "  \"discriminator\": {},", self.discriminator).unwrap();
246        s.push_str("  \"layout_id\": \"");
247        for byte in &self.layout_id {
248            write!(s, "{byte:02x}").unwrap();
249        }
250        s.push_str("\",\n");
251        writeln!(s, "  \"total_size\": {},", self.total_size()).unwrap();
252        s.push_str("  \"fields\": [\n");
253        let mut offset = 0usize;
254        for (i, field) in self.fields.iter().enumerate() {
255            let type_str = match field.canonical_type {
256                CanonicalType::Bytes(n) => {
257                    let mut t = String::from("bytes{");
258                    write!(t, "{n}").unwrap();
259                    t.push('}');
260                    t
261                }
262                other => String::from(other.as_str()),
263            };
264            write!(
265                s,
266                "    {{ \"name\": \"{}\", \"type\": \"{}\", \"size\": {}, \"offset\": {} }}",
267                field.name, type_str, field.size, offset,
268            )
269            .unwrap();
270            if i + 1 < self.fields.len() {
271                s.push(',');
272            }
273            s.push('\n');
274            offset += field.size;
275        }
276        if self.segments.is_empty() {
277            s.push_str("  ]\n");
278        } else {
279            s.push_str("  ],\n");
280            s.push_str("  \"segments\": [\n");
281            for (i, seg) in self.segments.iter().enumerate() {
282                write!(
283                    s,
284                    "    {{ \"name\": \"{}\", \"element_type\": \"{}\", \"element_size\": {} }}",
285                    seg.name, seg.element_type, seg.element_size,
286                )
287                .unwrap();
288                if i + 1 < self.segments.len() {
289                    s.push(',');
290                }
291                s.push('\n');
292            }
293            s.push_str("  ]\n");
294        }
295        s.push('}');
296        s
297    }
298
299    /// Validate structural invariants of this manifest.
300    ///
301    /// Returns `Ok(())` if:
302    /// - All field sizes are non-zero
303    /// - The first field is a `Header` with size 16
304    /// - No duplicate field names exist
305    /// - `total_size()` equals the sum of field sizes
306    ///
307    /// This does **not** recompute `layout_id` (that would require a
308    /// SHA-256 dependency). Use [`hash_input()`](Self::hash_input) to
309    /// verify the hash externally.
310    #[cfg(feature = "std")]
311    pub fn verify(&self) -> Result<(), String> {
312        if self.fields.is_empty() {
313            return Err("manifest has no fields".into());
314        }
315
316        // First field must be the header.
317        let first = &self.fields[0];
318        if first.canonical_type != CanonicalType::Header || first.size != 16 {
319            return Err(format!(
320                "first field must be Header(16), got {:?}({})",
321                first.canonical_type, first.size,
322            ));
323        }
324
325        // All sizes must be non-zero.
326        for field in self.fields {
327            if field.size == 0 {
328                return Err(format!("field '{}' has zero size", field.name));
329            }
330        }
331
332        // No duplicate names.
333        for (i, a) in self.fields.iter().enumerate() {
334            for b in &self.fields[i + 1..] {
335                if a.name == b.name {
336                    return Err(format!("duplicate field name '{}'", a.name));
337                }
338            }
339        }
340
341        Ok(())
342    }
343
344    /// Verify that raw account data matches this manifest.
345    ///
346    /// Checks:
347    /// - Data length ≥ `total_size()`
348    /// - Discriminator byte (offset 0) matches `self.discriminator`
349    /// - Layout ID bytes (offsets 4..12) match `self.layout_id`
350    ///
351    /// This is the runtime counterpart to `verify()` (which checks the
352    /// manifest's internal consistency). Use this to validate that
353    /// on-chain data belongs to the expected account type.
354    pub fn verify_account(&self, data: &[u8]) -> Result<(), &'static str> {
355        let expected_size = self.total_size();
356        if data.len() < expected_size {
357            return Err("account data too small for manifest");
358        }
359        if data[0] != self.discriminator {
360            return Err("discriminator mismatch");
361        }
362        if data.len() < 12 {
363            return Err("account data too small for header");
364        }
365        if data[4..12] != self.layout_id {
366            return Err("layout_id mismatch");
367        }
368        Ok(())
369    }
370
371    /// Verify that a caller-provided SHA-256 hash is consistent with
372    /// this manifest's `layout_id`.
373    ///
374    /// The caller computes `SHA-256(self.hash_input())` using their own
375    /// SHA-256 implementation and passes the full 32-byte digest here.
376    /// This method checks that the first 8 bytes match `self.layout_id`,
377    /// providing 256-bit collision resistance without adding a SHA-256
378    /// dependency to this crate.
379    ///
380    /// Returns `Ok(())` if the truncated hash matches, or an error
381    /// message if it doesn't.
382    pub fn verify_hash(&self, full_sha256: &[u8; 32]) -> Result<(), &'static str> {
383        if full_sha256[..8] != self.layout_id {
384            return Err("layout_id does not match truncated SHA-256");
385        }
386        Ok(())
387    }
388}
389
390/// Macro to generate a `LayoutManifest` from a `zero_copy_layout!` struct.
391///
392/// ```rust,ignore
393/// use jiminy_schema::layout_manifest;
394///
395/// let manifest = layout_manifest!(Vault,
396///     header:    Header  = 16,
397///     balance:   U64     = 8,
398///     authority: Pubkey  = 32,
399/// );
400/// ```
401#[macro_export]
402macro_rules! layout_manifest {
403    (
404        $name:ident,
405        $( $field:ident : $ctype:ident = $size:expr ),+ $(,)?
406    ) => {
407        $crate::LayoutManifest {
408            name: stringify!($name),
409            version: $name::VERSION,
410            discriminator: $name::DISC,
411            layout_id: $name::LAYOUT_ID,
412            fields: &[
413                $( $crate::FieldDescriptor {
414                    name: stringify!($field),
415                    canonical_type: $crate::CanonicalType::$ctype,
416                    size: $size,
417                }, )+
418            ],
419            segments: &[],
420        }
421    };
422    // Segmented variant: fixed fields + dynamic segments.
423    (
424        $name:ident,
425        $( $field:ident : $ctype:ident = $size:expr ),+ $(,)?
426        ;
427        segments { $( $seg_name:ident : $seg_elem_type:ident = $seg_elem_size:expr ),+ $(,)? }
428    ) => {
429        $crate::LayoutManifest {
430            name: stringify!($name),
431            version: $name::VERSION,
432            discriminator: $name::DISC,
433            layout_id: $name::SEGMENTED_LAYOUT_ID,
434            fields: &[
435                $( $crate::FieldDescriptor {
436                    name: stringify!($field),
437                    canonical_type: $crate::CanonicalType::$ctype,
438                    size: $size,
439                }, )+
440            ],
441            segments: &[
442                $( $crate::SegmentFieldDescriptor {
443                    name: stringify!($seg_name),
444                    element_type: stringify!($seg_elem_type),
445                    element_size: $seg_elem_size,
446                }, )+
447            ],
448        }
449    };
450}
451
452#[cfg(test)]
453mod tests {
454    use super::*;
455
456    #[test]
457    fn total_size_sums_fields() {
458        let manifest = LayoutManifest {
459            name: "Test",
460            version: 1,
461            discriminator: 1,
462            layout_id: [0; 8],
463            fields: &[
464                FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
465                FieldDescriptor { name: "value", canonical_type: CanonicalType::U64, size: 8 },
466            ],
467            segments: &[],
468        };
469        assert_eq!(manifest.total_size(), 24);
470    }
471
472    #[test]
473    fn field_offset_finds_correct_position() {
474        let manifest = LayoutManifest {
475            name: "Test",
476            version: 1,
477            discriminator: 1,
478            layout_id: [0; 8],
479            fields: &[
480                FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
481                FieldDescriptor { name: "balance", canonical_type: CanonicalType::U64, size: 8 },
482                FieldDescriptor { name: "authority", canonical_type: CanonicalType::Pubkey, size: 32 },
483            ],
484            segments: &[],
485        };
486        assert_eq!(manifest.field_offset("header"), Some(0));
487        assert_eq!(manifest.field_offset("balance"), Some(16));
488        assert_eq!(manifest.field_offset("authority"), Some(24));
489        assert_eq!(manifest.field_offset("nonexistent"), None);
490    }
491
492    #[test]
493    fn hash_input_format() {
494        let manifest = LayoutManifest {
495            name: "Vault",
496            version: 1,
497            discriminator: 1,
498            layout_id: [0; 8],
499            fields: &[
500                FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
501                FieldDescriptor { name: "balance", canonical_type: CanonicalType::U64, size: 8 },
502                FieldDescriptor { name: "authority", canonical_type: CanonicalType::Pubkey, size: 32 },
503            ],
504            segments: &[],
505        };
506        let input = manifest.hash_input();
507        assert_eq!(input, "jiminy:v1:Vault:1:header:header:16,balance:u64:8,authority:pubkey:32,");
508    }
509
510    #[test]
511    fn canonical_type_string_roundtrip() {
512        assert_eq!(CanonicalType::U8.as_str(), "u8");
513        assert_eq!(CanonicalType::U64.as_str(), "u64");
514        assert_eq!(CanonicalType::Pubkey.as_str(), "pubkey");
515        assert_eq!(CanonicalType::Header.as_str(), "header");
516        assert_eq!(CanonicalType::Bool.as_str(), "bool");
517        assert_eq!(CanonicalType::I128.as_str(), "i128");
518    }
519
520    #[test]
521    fn export_json_structure() {
522        let manifest = LayoutManifest {
523            name: "Vault",
524            version: 1,
525            discriminator: 1,
526            layout_id: [0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67, 0x89],
527            fields: &[
528                FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
529                FieldDescriptor { name: "balance", canonical_type: CanonicalType::U64, size: 8 },
530            ],
531            segments: &[],
532        };
533        let json = manifest.export_json();
534        assert!(json.contains("\"name\": \"Vault\""));
535        assert!(json.contains("\"total_size\": 24"));
536        assert!(json.contains("\"layout_id\": \"abcdef0123456789\""));
537        assert!(json.contains("\"offset\": 0"));
538        assert!(json.contains("\"offset\": 16"));
539    }
540
541    #[test]
542    fn verify_valid_manifest() {
543        let manifest = LayoutManifest {
544            name: "Vault",
545            version: 1,
546            discriminator: 1,
547            layout_id: [0; 8],
548            fields: &[
549                FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
550                FieldDescriptor { name: "balance", canonical_type: CanonicalType::U64, size: 8 },
551            ],
552            segments: &[],
553        };
554        assert!(manifest.verify().is_ok());
555    }
556
557    #[test]
558    fn verify_rejects_no_header() {
559        let manifest = LayoutManifest {
560            name: "Bad",
561            version: 1,
562            discriminator: 1,
563            layout_id: [0; 8],
564            fields: &[
565                FieldDescriptor { name: "balance", canonical_type: CanonicalType::U64, size: 8 },
566            ],
567            segments: &[],
568        };
569        assert!(manifest.verify().is_err());
570    }
571
572    #[test]
573    fn verify_rejects_duplicate_names() {
574        let manifest = LayoutManifest {
575            name: "Bad",
576            version: 1,
577            discriminator: 1,
578            layout_id: [0; 8],
579            fields: &[
580                FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
581                FieldDescriptor { name: "x", canonical_type: CanonicalType::U64, size: 8 },
582                FieldDescriptor { name: "x", canonical_type: CanonicalType::U32, size: 4 },
583            ],
584            segments: &[],
585        };
586        let err = manifest.verify().unwrap_err();
587        assert!(err.contains("duplicate"));
588    }
589
590    #[test]
591    fn verify_rejects_empty() {
592        let manifest = LayoutManifest {
593            name: "Empty",
594            version: 1,
595            discriminator: 1,
596            layout_id: [0; 8],
597            fields: &[],
598            segments: &[],
599        };
600        assert!(manifest.verify().is_err());
601    }
602
603    #[test]
604    fn verify_rejects_zero_size_field() {
605        let manifest = LayoutManifest {
606            name: "Bad",
607            version: 1,
608            discriminator: 1,
609            layout_id: [0; 8],
610            fields: &[
611                FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
612                FieldDescriptor { name: "empty", canonical_type: CanonicalType::U8, size: 0 },
613            ],
614            segments: &[],
615        };
616        let err = manifest.verify().unwrap_err();
617        assert!(err.contains("zero size"));
618    }
619
620    #[test]
621    fn field_count_returns_number_of_fields() {
622        let manifest = LayoutManifest {
623            name: "Test",
624            version: 1,
625            discriminator: 1,
626            layout_id: [0; 8],
627            fields: &[
628                FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
629                FieldDescriptor { name: "a", canonical_type: CanonicalType::U64, size: 8 },
630                FieldDescriptor { name: "b", canonical_type: CanonicalType::U32, size: 4 },
631            ],
632            segments: &[],
633        };
634        assert_eq!(manifest.field_count(), 3);
635    }
636
637    #[test]
638    fn field_lookup_returns_descriptor() {
639        let manifest = LayoutManifest {
640            name: "Test",
641            version: 1,
642            discriminator: 1,
643            layout_id: [0; 8],
644            fields: &[
645                FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
646                FieldDescriptor { name: "amount", canonical_type: CanonicalType::U64, size: 8 },
647            ],
648            segments: &[],
649        };
650        let f = manifest.field("amount").unwrap();
651        assert_eq!(f.canonical_type, CanonicalType::U64);
652        assert_eq!(f.size, 8);
653        assert!(manifest.field("nonexistent").is_none());
654    }
655
656    #[test]
657    fn hash_input_with_segments() {
658        let manifest = LayoutManifest {
659            name: "Pool",
660            version: 1,
661            discriminator: 3,
662            layout_id: [0; 8],
663            fields: &[
664                FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
665                FieldDescriptor { name: "total", canonical_type: CanonicalType::U64, size: 8 },
666            ],
667            segments: &[
668                SegmentFieldDescriptor { name: "stakes", element_type: "StakeEntry", element_size: 48 },
669            ],
670        };
671        let input = manifest.hash_input();
672        assert!(input.contains("seg:stakes:StakeEntry:48,"));
673        assert!(input.starts_with("jiminy:v1:Pool:1:"));
674    }
675
676    #[test]
677    fn hash_input_with_bytes_field() {
678        let manifest = LayoutManifest {
679            name: "Buffer",
680            version: 1,
681            discriminator: 2,
682            layout_id: [0; 8],
683            fields: &[
684                FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
685                FieldDescriptor { name: "data", canonical_type: CanonicalType::Bytes(64), size: 64 },
686            ],
687            segments: &[],
688        };
689        let input = manifest.hash_input();
690        assert!(input.contains("data:bytes{64}:64,"));
691    }
692
693    #[test]
694    fn export_json_with_segments() {
695        let manifest = LayoutManifest {
696            name: "OrderBook",
697            version: 1,
698            discriminator: 5,
699            layout_id: [0x11; 8],
700            fields: &[
701                FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
702                FieldDescriptor { name: "base", canonical_type: CanonicalType::Pubkey, size: 32 },
703            ],
704            segments: &[
705                SegmentFieldDescriptor { name: "bids", element_type: "Order", element_size: 48 },
706                SegmentFieldDescriptor { name: "asks", element_type: "Order", element_size: 48 },
707            ],
708        };
709        let json = manifest.export_json();
710        assert!(json.contains("\"segments\":"));
711        assert!(json.contains("\"name\": \"bids\""));
712        assert!(json.contains("\"element_type\": \"Order\""));
713        assert!(json.contains("\"element_size\": 48"));
714    }
715
716    #[test]
717    fn canonical_type_bytes_as_str() {
718        // Bytes variant always returns "bytes" — caller appends size
719        assert_eq!(CanonicalType::Bytes(32).as_str(), "bytes");
720        assert_eq!(CanonicalType::Bytes(128).as_str(), "bytes");
721    }
722
723    #[test]
724    fn verify_account_accepts_valid_data() {
725        let layout_id = [0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67, 0x89];
726        let manifest = LayoutManifest {
727            name: "Vault",
728            version: 1,
729            discriminator: 1,
730            layout_id,
731            fields: &[
732                FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
733                FieldDescriptor { name: "balance", canonical_type: CanonicalType::U64, size: 8 },
734            ],
735            segments: &[],
736        };
737        let mut data = vec![0u8; 24];
738        data[0] = 1; // disc
739        data[4..12].copy_from_slice(&layout_id);
740        assert!(manifest.verify_account(&data).is_ok());
741    }
742
743    #[test]
744    fn verify_account_rejects_wrong_disc() {
745        let layout_id = [0xAB; 8];
746        let manifest = LayoutManifest {
747            name: "Vault",
748            version: 1,
749            discriminator: 1,
750            layout_id,
751            fields: &[
752                FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
753                FieldDescriptor { name: "balance", canonical_type: CanonicalType::U64, size: 8 },
754            ],
755            segments: &[],
756        };
757        let mut data = vec![0u8; 24];
758        data[0] = 99; // wrong disc
759        data[4..12].copy_from_slice(&layout_id);
760        assert_eq!(manifest.verify_account(&data).unwrap_err(), "discriminator mismatch");
761    }
762
763    #[test]
764    fn verify_account_rejects_wrong_layout_id() {
765        let manifest = LayoutManifest {
766            name: "Vault",
767            version: 1,
768            discriminator: 1,
769            layout_id: [0xAB; 8],
770            fields: &[
771                FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
772                FieldDescriptor { name: "balance", canonical_type: CanonicalType::U64, size: 8 },
773            ],
774            segments: &[],
775        };
776        let mut data = vec![0u8; 24];
777        data[0] = 1;
778        data[4..12].copy_from_slice(&[0xFF; 8]); // wrong layout_id
779        assert_eq!(manifest.verify_account(&data).unwrap_err(), "layout_id mismatch");
780    }
781
782    #[test]
783    fn verify_account_rejects_too_small() {
784        let manifest = LayoutManifest {
785            name: "Vault",
786            version: 1,
787            discriminator: 1,
788            layout_id: [0; 8],
789            fields: &[
790                FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
791                FieldDescriptor { name: "balance", canonical_type: CanonicalType::U64, size: 8 },
792            ],
793            segments: &[],
794        };
795        let data = vec![0u8; 10]; // too small for 24-byte layout
796        assert_eq!(manifest.verify_account(&data).unwrap_err(), "account data too small for manifest");
797    }
798
799    #[test]
800    fn manifest_version_is_v1() {
801        assert_eq!(MANIFEST_VERSION, "manifest-v1");
802    }
803
804    #[test]
805    fn verify_hash_accepts_matching_prefix() {
806        let layout_id = [0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67, 0x89];
807        let manifest = LayoutManifest {
808            name: "Test",
809            version: 1,
810            discriminator: 1,
811            layout_id,
812            fields: &[
813                FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
814            ],
815            segments: &[],
816        };
817        let mut hash = [0u8; 32];
818        hash[..8].copy_from_slice(&layout_id);
819        // Rest can be anything — only first 8 bytes matter.
820        hash[8] = 0xFF;
821        assert!(manifest.verify_hash(&hash).is_ok());
822    }
823
824    #[test]
825    fn verify_hash_rejects_mismatch() {
826        let manifest = LayoutManifest {
827            name: "Test",
828            version: 1,
829            discriminator: 1,
830            layout_id: [0xAA; 8],
831            fields: &[
832                FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
833            ],
834            segments: &[],
835        };
836        let wrong_hash = [0xBB; 32];
837        assert!(manifest.verify_hash(&wrong_hash).is_err());
838    }
839
840    #[test]
841    fn export_json_contains_manifest_version() {
842        let manifest = LayoutManifest {
843            name: "V",
844            version: 1,
845            discriminator: 1,
846            layout_id: [0; 8],
847            fields: &[
848                FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
849            ],
850            segments: &[],
851        };
852        let json = manifest.export_json();
853        assert!(json.contains(&format!("\"version\": \"{}\"", MANIFEST_VERSION)));
854    }
855}