Skip to main content

jiminy_layouts/
lib.rs

1//! # jiminy-layouts
2//!
3//! Standard zero-copy account layouts for well-known Solana programs.
4//!
5//! This crate provides `#[repr(C)]` structs with [`Pod`] and [`FixedLayout`]
6//! implementations for SPL Token accounts, Mint accounts, and other
7//! widely-used on-chain data structures. These layouts are compatible
8//! with `jiminy-core`'s `pod_from_bytes()` for direct zero-copy access.
9//!
10//! ## Why a separate crate?
11//!
12//! `jiminy-solana` provides *function-based* field readers (e.g.,
13//! `token_account_owner(account)`). This crate provides *struct-based*
14//! overlays that map the entire account into a typed struct - useful when
15//! you need to read multiple fields efficiently.
16//!
17//! ## Layouts
18//!
19//! | Struct | Program | Size |
20//! |--------|---------|------|
21//! | [`SplTokenAccount`] | SPL Token | 165 bytes |
22//! | [`SplMint`] | SPL Token | 82 bytes |
23//! | [`SplMultisig`] | SPL Token | 355 bytes |
24//! | [`NonceAccount`] | System program | 80 bytes |
25//! | [`StakeState`] | Stake program | 200 bytes |
26//!
27//! ## Coverage philosophy
28//!
29//! This crate targets account types that programs commonly *read*
30//! cross-program. SPL Token accounts dominate that set. Nonce and
31//! Stake accounts are included because staking programs and
32//! durable-transaction workflows frequently inspect them.
33//! Additional layouts (e.g., Metaplex Token Metadata) can be added
34//! as the ecosystem matures.
35//!
36//! ## Example
37//!
38//! ```rust,ignore
39//! use jiminy_layouts::SplTokenAccount;
40//! use jiminy_core::account::{pod_from_bytes, FixedLayout};
41//!
42//! let data: &[u8] = &account.data;
43//! let token = pod_from_bytes::<SplTokenAccount>(data)?;
44//! let owner = token.owner;
45//! let amount = u64::from_le_bytes(token.amount);
46//! ```
47//!
48//! ## Important
49//!
50//! These are **external** (non-Jiminy) account layouts - they do NOT have
51//! the Jiminy 16-byte header. They are meant for reading accounts owned
52//! by other programs (SPL Token, System, Stake, etc.).
53
54#![no_std]
55
56use jiminy_core::account::{Pod, FixedLayout};
57
58// ── SPL Token Account ────────────────────────────────────────────────────────
59
60/// Zero-copy overlay for an SPL Token account (165 bytes).
61///
62/// Layout:
63/// ```text
64///   0..32   mint           [u8; 32]
65///  32..64   owner          [u8; 32]
66///  64..72   amount         [u8; 8]   (u64 LE)
67///  72..76   delegate_tag   [u8; 4]   (u32 LE: 0=None, 1=Some)
68///  76..108  delegate       [u8; 32]
69/// 108..109  state          u8        (0=uninit, 1=init, 2=frozen)
70/// 109..113  is_native_tag  [u8; 4]   (u32 LE: 0=None, 1=Some)
71/// 113..121  native_amount  [u8; 8]   (u64 LE)
72/// 121..129  delegated_amt  [u8; 8]   (u64 LE)
73/// 129..133  close_auth_tag [u8; 4]   (u32 LE: 0=None, 1=Some)
74/// 133..165  close_auth     [u8; 32]
75/// ```
76#[repr(C)]
77#[derive(Clone, Copy)]
78pub struct SplTokenAccount {
79    /// Mint address associated with this token account.
80    pub mint: [u8; 32],
81    /// Owner of this token account.
82    pub owner: [u8; 32],
83    /// Token balance (u64 LE).
84    pub amount: [u8; 8],
85    /// Delegate option tag (u32 LE: 0=None, 1=Some).
86    pub delegate_tag: [u8; 4],
87    /// Delegate address (valid only if delegate_tag == 1).
88    pub delegate: [u8; 32],
89    /// Account state: 0=Uninitialized, 1=Initialized, 2=Frozen.
90    pub state: u8,
91    /// Is-native option tag (u32 LE).
92    pub is_native_tag: [u8; 4],
93    /// Native SOL amount (u64 LE, valid only if is_native_tag == 1).
94    pub native_amount: [u8; 8],
95    /// Delegated amount (u64 LE).
96    pub delegated_amount: [u8; 8],
97    /// Close authority option tag (u32 LE).
98    pub close_authority_tag: [u8; 4],
99    /// Close authority address (valid only if close_authority_tag == 1).
100    pub close_authority: [u8; 32],
101}
102
103// SAFETY: SplTokenAccount is #[repr(C)], Copy, all fields are byte arrays,
104// and all bit patterns are valid.
105unsafe impl Pod for SplTokenAccount {}
106impl FixedLayout for SplTokenAccount { const SIZE: usize = 165; }
107
108impl SplTokenAccount {
109    /// Read the token amount as a u64.
110    #[inline(always)]
111    pub fn amount(&self) -> u64 {
112        u64::from_le_bytes(self.amount)
113    }
114
115    /// Check whether a delegate is set.
116    #[inline(always)]
117    pub fn has_delegate(&self) -> bool {
118        u32::from_le_bytes(self.delegate_tag) == 1
119    }
120
121    /// Check whether this is a native (SOL-wrapped) token account.
122    #[inline(always)]
123    pub fn is_native(&self) -> bool {
124        u32::from_le_bytes(self.is_native_tag) == 1
125    }
126
127    /// Check whether a close authority is set.
128    #[inline(always)]
129    pub fn has_close_authority(&self) -> bool {
130        u32::from_le_bytes(self.close_authority_tag) == 1
131    }
132
133    /// Check if the account is initialized.
134    #[inline(always)]
135    pub fn is_initialized(&self) -> bool {
136        self.state == 1
137    }
138
139    /// Check if the account is frozen.
140    #[inline(always)]
141    pub fn is_frozen(&self) -> bool {
142        self.state == 2
143    }
144}
145
146// ── SPL Mint ─────────────────────────────────────────────────────────────────
147
148/// Zero-copy overlay for an SPL Token mint account (82 bytes).
149///
150/// Layout:
151/// ```text
152///  0..4    mint_authority_tag  [u8; 4] (u32 LE: 0=None, 1=Some)
153///  4..36   mint_authority      [u8; 32]
154/// 36..44   supply              [u8; 8] (u64 LE)
155/// 44       decimals            u8
156/// 45       is_initialized      u8 (bool)
157/// 46..50   freeze_auth_tag     [u8; 4] (u32 LE: 0=None, 1=Some)
158/// 50..82   freeze_authority    [u8; 32]
159/// ```
160#[repr(C)]
161#[derive(Clone, Copy)]
162pub struct SplMint {
163    /// Mint authority option tag (u32 LE).
164    pub mint_authority_tag: [u8; 4],
165    /// Mint authority address (valid only if tag == 1).
166    pub mint_authority: [u8; 32],
167    /// Total supply (u64 LE).
168    pub supply: [u8; 8],
169    /// Number of decimals.
170    pub decimals: u8,
171    /// Whether the mint is initialized.
172    pub is_initialized: u8,
173    /// Freeze authority option tag (u32 LE).
174    pub freeze_authority_tag: [u8; 4],
175    /// Freeze authority address (valid only if tag == 1).
176    pub freeze_authority: [u8; 32],
177}
178
179// SAFETY: SplMint is #[repr(C)], Copy, all fields are byte arrays/u8.
180unsafe impl Pod for SplMint {}
181impl FixedLayout for SplMint { const SIZE: usize = 82; }
182
183impl SplMint {
184    /// Read the total supply as a u64.
185    #[inline(always)]
186    pub fn supply(&self) -> u64 {
187        u64::from_le_bytes(self.supply)
188    }
189
190    /// Check whether a mint authority is set.
191    #[inline(always)]
192    pub fn has_mint_authority(&self) -> bool {
193        u32::from_le_bytes(self.mint_authority_tag) == 1
194    }
195
196    /// Check whether a freeze authority is set.
197    #[inline(always)]
198    pub fn has_freeze_authority(&self) -> bool {
199        u32::from_le_bytes(self.freeze_authority_tag) == 1
200    }
201}
202
203// ── SPL Multisig ─────────────────────────────────────────────────────────────
204
205/// Zero-copy overlay for an SPL Token multisig account (355 bytes).
206///
207/// Layout:
208/// ```text
209///   0       m                u8  (signatures required)
210///   1       n                u8  (total signers)
211///   2       is_initialized   u8  (bool)
212///   3..355  signers          [u8; 352] (11 × 32-byte addresses)
213/// ```
214#[repr(C)]
215#[derive(Clone, Copy)]
216pub struct SplMultisig {
217    /// Number of signers required.
218    pub m: u8,
219    /// Total number of valid signers.
220    pub n: u8,
221    /// Whether the multisig is initialized.
222    pub is_initialized: u8,
223    /// Signer addresses (up to 11, 32 bytes each).
224    pub signers: [u8; 352],
225}
226
227// SAFETY: SplMultisig is #[repr(C)], Copy, all fields are u8/byte arrays.
228unsafe impl Pod for SplMultisig {}
229impl FixedLayout for SplMultisig { const SIZE: usize = 355; }
230
231impl SplMultisig {
232    /// Get the address of signer at index `i`.
233    ///
234    /// Returns `None` if `i >= n` or `i >= 11`.
235    #[inline(always)]
236    pub fn signer(&self, i: usize) -> Option<&[u8; 32]> {
237        if i >= self.n as usize || i >= 11 {
238            return None;
239        }
240        let start = i * 32;
241        // SAFETY: i < 11 so start+32 <= 352
242        Some(unsafe { &*(self.signers.as_ptr().add(start) as *const [u8; 32]) })
243    }
244}
245
246// ── System Nonce Account ─────────────────────────────────────────────────────
247
248/// Zero-copy overlay for a system program durable nonce account (80 bytes).
249///
250/// Layout:
251/// ```text
252///   0..4    version        [u8; 4]  (u32 LE: 0=Uninitialized, 1=Current)
253///   4..8    state          [u8; 4]  (u32 LE: 0=Uninitialized, 1=Initialized)
254///   8..40   authority      [u8; 32]
255///  40..72   blockhash      [u8; 32]
256///  72..80   lamports_per_sig [u8; 8] (u64 LE)
257/// ```
258#[repr(C)]
259#[derive(Clone, Copy)]
260pub struct NonceAccount {
261    /// Nonce version (u32 LE).
262    pub version: [u8; 4],
263    /// Nonce state (u32 LE: 0=Uninitialized, 1=Initialized).
264    pub state: [u8; 4],
265    /// Authority authorized to advance the nonce.
266    pub authority: [u8; 32],
267    /// Stored durable blockhash.
268    pub blockhash: [u8; 32],
269    /// Lamports per signature at the time the nonce was stored (u64 LE).
270    pub lamports_per_signature: [u8; 8],
271}
272
273// SAFETY: NonceAccount is #[repr(C)], Copy, all fields are byte arrays,
274// and all bit patterns are valid.
275unsafe impl Pod for NonceAccount {}
276impl FixedLayout for NonceAccount { const SIZE: usize = 80; }
277
278impl NonceAccount {
279    /// Check whether the nonce is initialized.
280    #[inline(always)]
281    pub fn is_initialized(&self) -> bool {
282        u32::from_le_bytes(self.state) == 1
283    }
284
285    /// Read lamports per signature as a u64.
286    #[inline(always)]
287    pub fn lamports_per_signature(&self) -> u64 {
288        u64::from_le_bytes(self.lamports_per_signature)
289    }
290}
291
292// ── Stake Account ────────────────────────────────────────────────────────────
293
294/// Zero-copy overlay for the fixed prefix of a stake account (200 bytes).
295///
296/// Covers the `Meta` portion of StakeState::Stake which is the section
297/// most programs need to read. The full StakeState (Stake variant) is
298/// 200 bytes total when including the Delegation.
299///
300/// Layout:
301/// ```text
302///   0..4    state             [u8; 4]  (u32 LE: enum discriminant)
303///   4..12   rent_exempt_reserve [u8; 8] (u64 LE)
304///  12..44   authorized_staker [u8; 32]
305///  44..76   authorized_withdrawer [u8; 32]
306///  76..84   lockup_timestamp  [u8; 8] (i64 LE: Unix timestamp)
307///  84..92   lockup_epoch      [u8; 8] (u64 LE)
308///  92..124  lockup_custodian  [u8; 32]
309/// 124..156  voter_pubkey      [u8; 32]
310/// 156..164  stake_amount      [u8; 8] (u64 LE)
311/// 164..172  activation_epoch  [u8; 8] (u64 LE)
312/// 172..180  deactivation_epoch [u8; 8] (u64 LE)
313/// 180..188  warmup_cooldown_rate [u8; 8] (f64 LE)
314/// 188..196  credits_observed  [u8; 8] (u64 LE)
315/// 196..200  _padding          [u8; 4]
316/// ```
317#[repr(C)]
318#[derive(Clone, Copy)]
319pub struct StakeState {
320    /// StakeState enum discriminant (u32 LE):
321    /// 0=Uninitialized, 1=Initialized, 2=Stake, 3=RewardsPool.
322    pub state: [u8; 4],
323    /// Rent-exempt reserve (u64 LE).
324    pub rent_exempt_reserve: [u8; 8],
325    /// Authorized staker pubkey.
326    pub authorized_staker: [u8; 32],
327    /// Authorized withdrawer pubkey.
328    pub authorized_withdrawer: [u8; 32],
329    /// Lockup Unix timestamp (i64 LE).
330    pub lockup_timestamp: [u8; 8],
331    /// Lockup epoch (u64 LE).
332    pub lockup_epoch: [u8; 8],
333    /// Lockup custodian pubkey.
334    pub lockup_custodian: [u8; 32],
335    /// Voter pubkey (valid when state == 2).
336    pub voter_pubkey: [u8; 32],
337    /// Delegated stake amount (u64 LE).
338    pub stake_amount: [u8; 8],
339    /// Activation epoch (u64 LE).
340    pub activation_epoch: [u8; 8],
341    /// Deactivation epoch (u64 LE, u64::MAX if not deactivating).
342    pub deactivation_epoch: [u8; 8],
343    /// Warmup/cooldown rate (f64 LE).
344    pub warmup_cooldown_rate: [u8; 8],
345    /// Credits observed (u64 LE).
346    pub credits_observed: [u8; 8],
347    /// Padding to 200 bytes.
348    pub _padding: [u8; 4],
349}
350
351// SAFETY: StakeState is #[repr(C)], Copy, all fields are byte arrays,
352// and all bit patterns are valid.
353unsafe impl Pod for StakeState {}
354impl FixedLayout for StakeState { const SIZE: usize = 200; }
355
356impl StakeState {
357    /// Get the state discriminant.
358    #[inline(always)]
359    pub fn state_kind(&self) -> u32 {
360        u32::from_le_bytes(self.state)
361    }
362
363    /// Check whether the stake is in the `Stake` state (delegated).
364    #[inline(always)]
365    pub fn is_delegated(&self) -> bool {
366        self.state_kind() == 2
367    }
368
369    /// Read the delegated stake amount as a u64.
370    #[inline(always)]
371    pub fn stake_amount(&self) -> u64 {
372        u64::from_le_bytes(self.stake_amount)
373    }
374
375    /// Read the activation epoch as a u64.
376    #[inline(always)]
377    pub fn activation_epoch(&self) -> u64 {
378        u64::from_le_bytes(self.activation_epoch)
379    }
380
381    /// Read the deactivation epoch as a u64.
382    /// Returns `u64::MAX` if not deactivating.
383    #[inline(always)]
384    pub fn deactivation_epoch(&self) -> u64 {
385        u64::from_le_bytes(self.deactivation_epoch)
386    }
387}
388
389// ── Compile-time size assertions ─────────────────────────────────────────────
390
391const _: () = assert!(core::mem::size_of::<SplTokenAccount>() == 165);
392const _: () = assert!(core::mem::size_of::<SplMint>() == 82);
393const _: () = assert!(core::mem::size_of::<SplMultisig>() == 355);
394const _: () = assert!(core::mem::size_of::<NonceAccount>() == 80);
395const _: () = assert!(core::mem::size_of::<StakeState>() == 200);
396
397// ── Compile-time alignment assertions ────────────────────────────────────────
398
399const _: () = assert!(core::mem::align_of::<SplTokenAccount>() == 1);
400const _: () = assert!(core::mem::align_of::<SplMint>() == 1);
401const _: () = assert!(core::mem::align_of::<SplMultisig>() == 1);
402const _: () = assert!(core::mem::align_of::<NonceAccount>() == 1);
403const _: () = assert!(core::mem::align_of::<StakeState>() == 1);
404
405#[cfg(test)]
406mod tests {
407    use super::*;
408    use jiminy_core::account::pod_from_bytes;
409
410    // ── Helper: write bytes and cast via pod_from_bytes ──────────────────
411
412    fn offset_of<T, F>(base: *const T, field: *const F) -> usize {
413        (field as usize) - (base as usize)
414    }
415
416    // ── SplTokenAccount ──────────────────────────────────────────────────
417
418    #[test]
419    fn token_account_size() {
420        assert_eq!(SplTokenAccount::SIZE, 165);
421        assert_eq!(core::mem::size_of::<SplTokenAccount>(), 165);
422    }
423
424    #[test]
425    fn token_account_field_offsets() {
426        let t = SplTokenAccount {
427            mint: [0; 32],
428            owner: [0; 32],
429            amount: [0; 8],
430            delegate_tag: [0; 4],
431            delegate: [0; 32],
432            state: 0,
433            is_native_tag: [0; 4],
434            native_amount: [0; 8],
435            delegated_amount: [0; 8],
436            close_authority_tag: [0; 4],
437            close_authority: [0; 32],
438        };
439        let base = &t as *const SplTokenAccount;
440        assert_eq!(offset_of(base, &t.mint as *const _), 0);
441        assert_eq!(offset_of(base, &t.owner as *const _), 32);
442        assert_eq!(offset_of(base, &t.amount as *const _), 64);
443        assert_eq!(offset_of(base, &t.delegate_tag as *const _), 72);
444        assert_eq!(offset_of(base, &t.delegate as *const _), 76);
445        assert_eq!(offset_of(base, &t.state as *const _), 108);
446        assert_eq!(offset_of(base, &t.is_native_tag as *const _), 109);
447        assert_eq!(offset_of(base, &t.native_amount as *const _), 113);
448        assert_eq!(offset_of(base, &t.delegated_amount as *const _), 121);
449        assert_eq!(offset_of(base, &t.close_authority_tag as *const _), 129);
450        assert_eq!(offset_of(base, &t.close_authority as *const _), 133);
451    }
452
453    #[test]
454    fn token_account_pod_roundtrip() {
455        let mut buf = [0u8; 165];
456        // Write a known mint at byte 0.
457        buf[0..32].copy_from_slice(&[0xAA; 32]);
458        // Write amount at byte 64.
459        buf[64..72].copy_from_slice(&1_000_000u64.to_le_bytes());
460        // Write state=Initialized at byte 108.
461        buf[108] = 1;
462        // Write delegate_tag=1 at byte 72.
463        buf[72..76].copy_from_slice(&1u32.to_le_bytes());
464        // Write delegate at byte 76.
465        buf[76..108].copy_from_slice(&[0xBB; 32]);
466
467        let token = pod_from_bytes::<SplTokenAccount>(&buf).unwrap();
468        assert_eq!(token.mint, [0xAA; 32]);
469        assert_eq!(token.amount(), 1_000_000);
470        assert!(token.is_initialized());
471        assert!(!token.is_frozen());
472        assert!(token.has_delegate());
473        assert_eq!(token.delegate, [0xBB; 32]);
474        assert!(!token.is_native());
475        assert!(!token.has_close_authority());
476    }
477
478    // ── SplMint ──────────────────────────────────────────────────────────
479
480    #[test]
481    fn mint_size() {
482        assert_eq!(SplMint::SIZE, 82);
483        assert_eq!(core::mem::size_of::<SplMint>(), 82);
484    }
485
486    #[test]
487    fn mint_field_offsets() {
488        let m = SplMint {
489            mint_authority_tag: [0; 4],
490            mint_authority: [0; 32],
491            supply: [0; 8],
492            decimals: 0,
493            is_initialized: 0,
494            freeze_authority_tag: [0; 4],
495            freeze_authority: [0; 32],
496        };
497        let base = &m as *const SplMint;
498        assert_eq!(offset_of(base, &m.mint_authority_tag as *const _), 0);
499        assert_eq!(offset_of(base, &m.mint_authority as *const _), 4);
500        assert_eq!(offset_of(base, &m.supply as *const _), 36);
501        assert_eq!(offset_of(base, &m.decimals as *const _), 44);
502        assert_eq!(offset_of(base, &m.is_initialized as *const _), 45);
503        assert_eq!(offset_of(base, &m.freeze_authority_tag as *const _), 46);
504        assert_eq!(offset_of(base, &m.freeze_authority as *const _), 50);
505    }
506
507    #[test]
508    fn mint_pod_roundtrip() {
509        let mut buf = [0u8; 82];
510        // mint_authority_tag=1 at 0..4
511        buf[0..4].copy_from_slice(&1u32.to_le_bytes());
512        // mint_authority at 4..36
513        buf[4..36].copy_from_slice(&[0xCC; 32]);
514        // supply at 36..44
515        buf[36..44].copy_from_slice(&1_000_000_000u64.to_le_bytes());
516        // decimals at 44
517        buf[44] = 9;
518        // is_initialized at 45
519        buf[45] = 1;
520
521        let mint = pod_from_bytes::<SplMint>(&buf).unwrap();
522        assert!(mint.has_mint_authority());
523        assert_eq!(mint.mint_authority, [0xCC; 32]);
524        assert_eq!(mint.supply(), 1_000_000_000);
525        assert_eq!(mint.decimals, 9);
526        assert!(!mint.has_freeze_authority());
527    }
528
529    // ── SplMultisig ──────────────────────────────────────────────────────
530
531    #[test]
532    fn multisig_size() {
533        assert_eq!(SplMultisig::SIZE, 355);
534        assert_eq!(core::mem::size_of::<SplMultisig>(), 355);
535    }
536
537    #[test]
538    fn multisig_field_offsets() {
539        let ms = SplMultisig {
540            m: 0,
541            n: 0,
542            is_initialized: 0,
543            signers: [0; 352],
544        };
545        let base = &ms as *const SplMultisig;
546        assert_eq!(offset_of(base, &ms.m as *const _), 0);
547        assert_eq!(offset_of(base, &ms.n as *const _), 1);
548        assert_eq!(offset_of(base, &ms.is_initialized as *const _), 2);
549        assert_eq!(offset_of(base, &ms.signers as *const _), 3);
550    }
551
552    #[test]
553    fn multisig_signer_access() {
554        let mut ms = SplMultisig {
555            m: 2,
556            n: 3,
557            is_initialized: 1,
558            signers: [0; 352],
559        };
560        ms.signers[0..32].copy_from_slice(&[0x11; 32]);
561        ms.signers[32..64].copy_from_slice(&[0x22; 32]);
562        ms.signers[64..96].copy_from_slice(&[0x33; 32]);
563
564        assert_eq!(ms.signer(0).unwrap(), &[0x11; 32]);
565        assert_eq!(ms.signer(1).unwrap(), &[0x22; 32]);
566        assert_eq!(ms.signer(2).unwrap(), &[0x33; 32]);
567        assert!(ms.signer(3).is_none()); // n=3, so index 3 is out
568        assert!(ms.signer(11).is_none()); // hard cap
569    }
570
571    // ── NonceAccount ─────────────────────────────────────────────────────
572
573    #[test]
574    fn nonce_account_size() {
575        assert_eq!(NonceAccount::SIZE, 80);
576        assert_eq!(core::mem::size_of::<NonceAccount>(), 80);
577    }
578
579    #[test]
580    fn nonce_account_field_offsets() {
581        let n = NonceAccount {
582            version: [0; 4],
583            state: [0; 4],
584            authority: [0; 32],
585            blockhash: [0; 32],
586            lamports_per_signature: [0; 8],
587        };
588        let base = &n as *const NonceAccount;
589        assert_eq!(offset_of(base, &n.version as *const _), 0);
590        assert_eq!(offset_of(base, &n.state as *const _), 4);
591        assert_eq!(offset_of(base, &n.authority as *const _), 8);
592        assert_eq!(offset_of(base, &n.blockhash as *const _), 40);
593        assert_eq!(offset_of(base, &n.lamports_per_signature as *const _), 72);
594    }
595
596    #[test]
597    fn nonce_account_pod_roundtrip() {
598        let mut buf = [0u8; 80];
599        // version=1 at 0..4
600        buf[0..4].copy_from_slice(&1u32.to_le_bytes());
601        // state=1 (init) at 4..8
602        buf[4..8].copy_from_slice(&1u32.to_le_bytes());
603        // authority at 8..40
604        buf[8..40].copy_from_slice(&[0xDD; 32]);
605        // lamports_per_sig at 72..80
606        buf[72..80].copy_from_slice(&5000u64.to_le_bytes());
607
608        let nonce = pod_from_bytes::<NonceAccount>(&buf).unwrap();
609        assert!(nonce.is_initialized());
610        assert_eq!(nonce.authority, [0xDD; 32]);
611        assert_eq!(nonce.lamports_per_signature(), 5000);
612    }
613
614    #[test]
615    fn nonce_account_initialized() {
616        let mut nonce = NonceAccount {
617            version: 1u32.to_le_bytes(),
618            state: 0u32.to_le_bytes(),
619            authority: [0; 32],
620            blockhash: [0; 32],
621            lamports_per_signature: [0; 8],
622        };
623        assert!(!nonce.is_initialized());
624        nonce.state = 1u32.to_le_bytes();
625        assert!(nonce.is_initialized());
626    }
627
628    // ── StakeState ───────────────────────────────────────────────────────
629
630    #[test]
631    fn stake_state_size() {
632        assert_eq!(StakeState::SIZE, 200);
633        assert_eq!(core::mem::size_of::<StakeState>(), 200);
634    }
635
636    #[test]
637    fn stake_state_field_offsets() {
638        let s = StakeState {
639            state: [0; 4],
640            rent_exempt_reserve: [0; 8],
641            authorized_staker: [0; 32],
642            authorized_withdrawer: [0; 32],
643            lockup_timestamp: [0; 8],
644            lockup_epoch: [0; 8],
645            lockup_custodian: [0; 32],
646            voter_pubkey: [0; 32],
647            stake_amount: [0; 8],
648            activation_epoch: [0; 8],
649            deactivation_epoch: [0; 8],
650            warmup_cooldown_rate: [0; 8],
651            credits_observed: [0; 8],
652            _padding: [0; 4],
653        };
654        let base = &s as *const StakeState;
655        assert_eq!(offset_of(base, &s.state as *const _), 0);
656        assert_eq!(offset_of(base, &s.rent_exempt_reserve as *const _), 4);
657        assert_eq!(offset_of(base, &s.authorized_staker as *const _), 12);
658        assert_eq!(offset_of(base, &s.authorized_withdrawer as *const _), 44);
659        assert_eq!(offset_of(base, &s.lockup_timestamp as *const _), 76);
660        assert_eq!(offset_of(base, &s.lockup_epoch as *const _), 84);
661        assert_eq!(offset_of(base, &s.lockup_custodian as *const _), 92);
662        assert_eq!(offset_of(base, &s.voter_pubkey as *const _), 124);
663        assert_eq!(offset_of(base, &s.stake_amount as *const _), 156);
664        assert_eq!(offset_of(base, &s.activation_epoch as *const _), 164);
665        assert_eq!(offset_of(base, &s.deactivation_epoch as *const _), 172);
666        assert_eq!(offset_of(base, &s.warmup_cooldown_rate as *const _), 180);
667        assert_eq!(offset_of(base, &s.credits_observed as *const _), 188);
668        assert_eq!(offset_of(base, &s._padding as *const _), 196);
669    }
670
671    #[test]
672    fn stake_state_pod_roundtrip() {
673        let mut buf = [0u8; 200];
674        // state=2 (Stake) at 0..4
675        buf[0..4].copy_from_slice(&2u32.to_le_bytes());
676        // voter_pubkey at 124..156
677        buf[124..156].copy_from_slice(&[0xEE; 32]);
678        // stake_amount at 156..164
679        buf[156..164].copy_from_slice(&5_000_000u64.to_le_bytes());
680        // activation_epoch at 164..172
681        buf[164..172].copy_from_slice(&42u64.to_le_bytes());
682        // deactivation_epoch at 172..180 (u64::MAX = not deactivating)
683        buf[172..180].copy_from_slice(&u64::MAX.to_le_bytes());
684
685        let stake = pod_from_bytes::<StakeState>(&buf).unwrap();
686        assert!(stake.is_delegated());
687        assert_eq!(stake.voter_pubkey, [0xEE; 32]);
688        assert_eq!(stake.stake_amount(), 5_000_000);
689        assert_eq!(stake.activation_epoch(), 42);
690        assert_eq!(stake.deactivation_epoch(), u64::MAX);
691    }
692
693    #[test]
694    fn stake_state_delegated() {
695        let mut stake = StakeState {
696            state: 2u32.to_le_bytes(),
697            rent_exempt_reserve: [0; 8],
698            authorized_staker: [0; 32],
699            authorized_withdrawer: [0; 32],
700            lockup_timestamp: [0; 8],
701            lockup_epoch: [0; 8],
702            lockup_custodian: [0; 32],
703            voter_pubkey: [0; 32],
704            stake_amount: 1_000_000u64.to_le_bytes(),
705            activation_epoch: 100u64.to_le_bytes(),
706            deactivation_epoch: u64::MAX.to_le_bytes(),
707            warmup_cooldown_rate: [0; 8],
708            credits_observed: [0; 8],
709            _padding: [0; 4],
710        };
711        assert!(stake.is_delegated());
712        assert_eq!(stake.stake_amount(), 1_000_000);
713        assert_eq!(stake.activation_epoch(), 100);
714        assert_eq!(stake.deactivation_epoch(), u64::MAX);
715
716        stake.state = 1u32.to_le_bytes(); // Initialized, not Stake
717        assert!(!stake.is_delegated());
718    }
719
720    // ── Cross-layout: raw bytes vs pod_from_bytes ────────────────────────
721
722    #[test]
723    fn token_account_bytes_match_struct() {
724        let token = SplTokenAccount {
725            mint: [1; 32],
726            owner: [2; 32],
727            amount: 42u64.to_le_bytes(),
728            delegate_tag: 0u32.to_le_bytes(),
729            delegate: [0; 32],
730            state: 1,
731            is_native_tag: 0u32.to_le_bytes(),
732            native_amount: [0; 8],
733            delegated_amount: [0; 8],
734            close_authority_tag: 0u32.to_le_bytes(),
735            close_authority: [0; 32],
736        };
737        // Cast struct to bytes and back via pod_from_bytes.
738        let bytes = unsafe {
739            core::slice::from_raw_parts(
740                &token as *const SplTokenAccount as *const u8,
741                165,
742            )
743        };
744        let roundtrip = pod_from_bytes::<SplTokenAccount>(bytes).unwrap();
745        assert_eq!(roundtrip.mint, [1; 32]);
746        assert_eq!(roundtrip.owner, [2; 32]);
747        assert_eq!(roundtrip.amount(), 42);
748        assert!(roundtrip.is_initialized());
749    }
750}