hopper_macros/lib.rs
1//! # Hopper Macros (Declarative)
2//!
3//! **Support infrastructure, not the main public entry.** These
4//! `macro_rules!` macros generate layout structure, field metadata, and
5//! validation primitives at compile time. They are deliberately limited
6//! to *structure* generation: no hidden runtime logic, no surprise
7//! control flow, no validation engines.
8//!
9//! Programs that want richer DX should enable the `proc-macros` feature
10//! and reach for `#[hopper::state]`, `#[hopper::context]`, and
11//! `#[hopper::program]` in `hopper-macros-proc`. Programs that prefer a
12//! zero-tool-chain authoring path can use these declarative macros
13//! directly, both paths lower to the same runtime.
14//!
15//! ## Topical index
16//!
17//! | Section | Macros |
18//! |--------------------|--------|
19//! | Layout | `hopper_layout!` |
20//! | Validation | `hopper_check!`, `hopper_error!`, `hopper_require!` |
21//! | Lifecycle | `hopper_init!`, `hopper_close!` |
22//! | Dispatch | `hopper_register_discs!` |
23//! | PDA | `hopper_verify_pda!` |
24//! | Invariants | `hopper_invariant!` |
25//! | Manifest | `hopper_manifest!` |
26//! | Segments | `hopper_segment!` |
27//! | Pipelines | `hopper_validate!` |
28//! | Virtual state | `hopper_virtual!` |
29//! | Compat / ABI | `hopper_assert_compatible!`, `hopper_assert_fingerprint!`, `const_assert_pod!` |
30//! | Cross-program | `hopper_interface!` |
31//! | Account structs | `hopper_accounts!` |
32//!
33//! Every macro below is `#[macro_export]`ed and usable from the root of
34//! the `hopper-macros` crate regardless of the section banner it lives
35//! under. The banners exist only to help readers navigate the file.
36
37#![no_std]
38
39// ═════════════════════════════════════════════════════════════════════
40// Section: Layout
41// ═════════════════════════════════════════════════════════════════════
42
43/// Define a zero-copy account layout.
44///
45/// Generates a `#[repr(C)]` struct with:
46/// - 16-byte Hopper header
47/// - Alignment-1 fields
48/// - Deterministic `LAYOUT_ID` via SHA-256
49/// - Tiered loading: `load`, `load_mut`, `load_cross_program`, `load_compatible`, `load_unverified`
50/// - Compile-time size and alignment assertions
51///
52/// # Example
53///
54/// ```ignore
55/// hopper_layout! {
56/// pub struct Vault, disc = 1, version = 1 {
57/// authority: [u8; 32] = 32,
58/// mint: [u8; 32] = 32,
59/// balance: WireU64 = 8,
60/// bump: u8 = 1,
61/// }
62/// }
63/// ```
64#[macro_export]
65macro_rules! hopper_layout {
66 (
67 $(#[$attr:meta])*
68 pub struct $name:ident, disc = $disc:literal, version = $ver:literal
69 {
70 $(
71 $(#[$field_attr:meta])*
72 $field:ident : $fty:ty = $fsize:literal
73 ),+ $(,)?
74 }
75 ) => {
76 $(#[$attr])*
77 #[derive(Clone, Copy)]
78 #[repr(C)]
79 pub struct $name {
80 pub header: $crate::hopper_core::account::AccountHeader,
81 $(
82 $(#[$field_attr])*
83 pub $field: $fty,
84 )+
85 }
86
87 // Compile-time assertions
88 const _: () = {
89 // Size check: header + sum of field sizes
90 let expected = $crate::hopper_core::account::HEADER_LEN $( + $fsize )+;
91 assert!(
92 core::mem::size_of::<$name>() == expected,
93 "Layout size mismatch: struct size != declared field sizes + header"
94 );
95 // Alignment-1 check
96 assert!(
97 core::mem::align_of::<$name>() == 1,
98 "Layout alignment must be 1 for zero-copy safety"
99 );
100 };
101
102 // Bytemuck proof (Hopper Safety Audit Must-Fix #5): every
103 // field must itself satisfy `bytemuck::Pod + Zeroable`.
104 // Hopper's Pod supertrait requires these impls; because all
105 // field types generated by `hopper_layout!` are Hopper wire
106 // types (which carry their own bytemuck impls) or byte
107 // arrays, these blanket claims are safe.
108 #[cfg(feature = "hopper-native-backend")]
109 unsafe impl $crate::hopper_runtime::__hopper_native::bytemuck::Zeroable for $name {}
110 #[cfg(feature = "hopper-native-backend")]
111 unsafe impl $crate::hopper_runtime::__hopper_native::bytemuck::Pod for $name {}
112
113 // SAFETY: #[repr(C)] over alignment-1 fields, all bit patterns valid
114 // for the constituent Pod types (header, wire integers, byte arrays).
115 unsafe impl $crate::hopper_core::account::Pod for $name {}
116
117 // Audit final-API Step 5 seal. `hopper_layout!` stamps the
118 // Hopper-authored marker so the `ZeroCopy` blanket picks up
119 // declarative layouts the same way it picks up `#[hopper::state]`
120 // ones.
121 unsafe impl $crate::hopper_runtime::__sealed::HopperZeroCopySealed for $name {}
122
123 impl $crate::hopper_core::account::FixedLayout for $name {
124 const SIZE: usize = $crate::hopper_core::account::HEADER_LEN $( + $fsize )+;
125 }
126
127 impl $crate::hopper_core::field_map::FieldMap for $name {
128 const FIELDS: &'static [$crate::hopper_core::field_map::FieldInfo] = {
129 const FIELD_COUNT: usize = 0 $( + { let _ = stringify!($field); 1 } )+;
130 const NAMES: [&str; FIELD_COUNT] = [ $( stringify!($field) ),+ ];
131 const SIZES: [usize; FIELD_COUNT] = [ $( $fsize ),+ ];
132 const FIELDS: [$crate::hopper_core::field_map::FieldInfo; FIELD_COUNT] = {
133 let mut result = [$crate::hopper_core::field_map::FieldInfo::new("", 0, 0); FIELD_COUNT];
134 let mut offset = $crate::hopper_core::account::HEADER_LEN;
135 let mut index = 0;
136 while index < FIELD_COUNT {
137 result[index] = $crate::hopper_core::field_map::FieldInfo::new(
138 NAMES[index],
139 offset,
140 SIZES[index],
141 );
142 offset += SIZES[index];
143 index += 1;
144 }
145 result
146 };
147 &FIELDS
148 };
149 }
150
151 impl $crate::hopper_runtime::LayoutContract for $name {
152 const DISC: u8 = $disc;
153 const VERSION: u8 = $ver;
154 const LAYOUT_ID: [u8; 8] = $name::LAYOUT_ID;
155 const SIZE: usize = $name::LEN;
156 const TYPE_OFFSET: usize = 0;
157 }
158
159 impl $crate::hopper_schema::SchemaExport for $name {
160 fn layout_manifest() -> $crate::hopper_schema::LayoutManifest {
161 const FIELD_COUNT: usize = 0 $( + { let _ = stringify!($field); 1 } )+;
162 const SIZES: [u16; FIELD_COUNT] = [ $( $fsize ),+ ];
163 const NAMES: [&str; FIELD_COUNT] = [ $( stringify!($field) ),+ ];
164 const TYPES: [&str; FIELD_COUNT] = [ $( stringify!($fty) ),+ ];
165 const FIELDS: [$crate::hopper_schema::FieldDescriptor; FIELD_COUNT] = {
166 let mut result = [$crate::hopper_schema::FieldDescriptor {
167 name: "", canonical_type: "", size: 0, offset: 0,
168 intent: $crate::hopper_schema::FieldIntent::Custom,
169 }; FIELD_COUNT];
170 let mut offset = $crate::hopper_core::account::HEADER_LEN as u16;
171 let mut index = 0;
172 while index < FIELD_COUNT {
173 result[index] = $crate::hopper_schema::FieldDescriptor {
174 name: NAMES[index],
175 canonical_type: TYPES[index],
176 size: SIZES[index],
177 offset,
178 intent: $crate::hopper_schema::FieldIntent::Custom,
179 };
180 offset += SIZES[index];
181 index += 1;
182 }
183 result
184 };
185 $crate::hopper_schema::LayoutManifest {
186 name: stringify!($name),
187 version: <$name>::VERSION,
188 disc: <$name>::DISC,
189 layout_id: <$name>::LAYOUT_ID,
190 total_size: <$name>::LEN,
191 field_count: FIELD_COUNT,
192 fields: &FIELDS,
193 }
194 }
195 }
196
197 impl $name {
198 /// Total byte size of this layout.
199 pub const LEN: usize = $crate::hopper_core::account::HEADER_LEN $( + $fsize )+;
200
201 /// Discriminator tag.
202 pub const DISC: u8 = $disc;
203
204 /// Layout version.
205 pub const VERSION: u8 = $ver;
206
207 /// Deterministic layout fingerprint.
208 ///
209 /// SHA-256 of: `"hopper:v1:Name:version:field:type:size,..."`
210 /// First 8 bytes for efficient comparison.
211 pub const LAYOUT_ID: [u8; 8] = {
212 // Build the canonical hash input at compile time
213 const INPUT: &str = concat!(
214 "hopper:v1:",
215 stringify!($name), ":",
216 stringify!($ver), ":",
217 $( stringify!($field), ":", stringify!($fty), ":", stringify!($fsize), ",", )+
218 );
219 const HASH: [u8; 32] = $crate::hopper_core::__sha256_const(INPUT.as_bytes());
220 [
221 HASH[0], HASH[1], HASH[2], HASH[3],
222 HASH[4], HASH[5], HASH[6], HASH[7],
223 ]
224 };
225
226 /// Zero-copy overlay over an already-borrowed byte slice (immutable).
227 ///
228 /// Prefer [`Self::load`] for account data so owner/header checks and
229 /// Hopper borrow tracking stay in force. Use this helper for tests,
230 /// scratch buffers, and explicitly sliced extension segments whose
231 /// surrounding account was already validated.
232 #[inline(always)]
233 pub fn overlay(data: &[u8]) -> Result<&Self, $crate::hopper_runtime::error::ProgramError> {
234 $crate::hopper_core::account::pod_from_bytes::<Self>(data)
235 }
236
237 /// Zero-copy overlay over an already-borrowed byte slice (mutable).
238 ///
239 /// Prefer [`Self::load_mut`] for account data so owner/header checks,
240 /// writable checks, and Hopper borrow tracking stay in force. Use this
241 /// helper for tests, scratch buffers, and explicitly sliced extension
242 /// segments whose surrounding account was already validated.
243 #[inline(always)]
244 pub fn overlay_mut(data: &mut [u8]) -> Result<&mut Self, $crate::hopper_runtime::error::ProgramError> {
245 $crate::hopper_core::account::pod_from_bytes_mut::<Self>(data)
246 }
247
248 /// Tier 1: Full validation load (own program accounts).
249 ///
250 /// Validates: owner + discriminator + version + layout_id + exact size.
251 #[inline]
252 pub fn load<'a>(
253 account: &'a $crate::hopper_runtime::AccountView,
254 program_id: &$crate::hopper_runtime::Address,
255 ) -> Result<
256 $crate::hopper_core::account::VerifiedAccount<'a, Self>,
257 $crate::hopper_runtime::error::ProgramError,
258 > {
259 $crate::hopper_core::check::check_owner(account, program_id)?;
260 let data = account.try_borrow()?;
261 $crate::hopper_core::account::check_header(
262 &*data,
263 Self::DISC,
264 Self::VERSION,
265 &Self::LAYOUT_ID,
266 )?;
267 $crate::hopper_core::check::check_size(&*data, Self::LEN)?;
268 $crate::hopper_core::account::VerifiedAccount::from_ref(data)
269 }
270
271 /// Tier 1m: Full validation load (mutable).
272 #[inline]
273 pub fn load_mut<'a>(
274 account: &'a $crate::hopper_runtime::AccountView,
275 program_id: &$crate::hopper_runtime::Address,
276 ) -> Result<
277 $crate::hopper_core::account::VerifiedAccountMut<'a, Self>,
278 $crate::hopper_runtime::error::ProgramError,
279 > {
280 $crate::hopper_core::check::check_owner(account, program_id)?;
281 $crate::hopper_core::check::check_writable(account)?;
282 let data = account.try_borrow_mut()?;
283 $crate::hopper_core::account::check_header(
284 &*data,
285 Self::DISC,
286 Self::VERSION,
287 &Self::LAYOUT_ID,
288 )?;
289 $crate::hopper_core::check::check_size(&*data, Self::LEN)?;
290 $crate::hopper_core::account::VerifiedAccountMut::from_ref_mut(data)
291 }
292
293 /// Tier 2: Foreign account load (cross-program reads).
294 ///
295 /// Validates: owner + layout_id + exact size (no disc/version check).
296 ///
297 /// **Deprecated:** Renamed to `load_cross_program()` for clarity.
298 #[deprecated(since = "0.2.0", note = "renamed to load_cross_program()")]
299 #[inline]
300 pub fn load_foreign<'a>(
301 account: &'a $crate::hopper_runtime::AccountView,
302 expected_owner: &$crate::hopper_runtime::Address,
303 ) -> Result<
304 $crate::hopper_core::account::VerifiedAccount<'a, Self>,
305 $crate::hopper_runtime::error::ProgramError,
306 > {
307 $crate::hopper_core::check::check_owner(account, expected_owner)?;
308 let data = account.try_borrow()?;
309 let layout_id = $crate::hopper_core::account::read_layout_id(&*data)?;
310 if layout_id != Self::LAYOUT_ID {
311 return Err($crate::hopper_runtime::error::ProgramError::InvalidAccountData);
312 }
313 $crate::hopper_core::check::check_size(&*data, Self::LEN)?;
314 $crate::hopper_core::account::VerifiedAccount::from_ref(data)
315 }
316
317 /// Cross-program account load (reads accounts owned by other programs).
318 ///
319 /// Validates: owner + layout_id + exact size (no disc/version check).
320 /// This is the successor to `load_foreign()` with a clearer name.
321 #[inline]
322 pub fn load_cross_program<'a>(
323 account: &'a $crate::hopper_runtime::AccountView,
324 expected_owner: &$crate::hopper_runtime::Address,
325 ) -> Result<
326 $crate::hopper_core::account::VerifiedAccount<'a, Self>,
327 $crate::hopper_runtime::error::ProgramError,
328 > {
329 $crate::hopper_core::check::check_owner(account, expected_owner)?;
330 let data = account.try_borrow()?;
331 let layout_id = $crate::hopper_core::account::read_layout_id(&*data)?;
332 if layout_id != Self::LAYOUT_ID {
333 return Err($crate::hopper_runtime::error::ProgramError::InvalidAccountData);
334 }
335 $crate::hopper_core::check::check_size(&*data, Self::LEN)?;
336 $crate::hopper_core::account::VerifiedAccount::from_ref(data)
337 }
338
339 /// Tier 3: Version-compatible load for migration scenarios.
340 ///
341 /// Validates: owner + discriminator + minimum version + minimum size.
342 /// Does **not** check layout_id, so it accepts any version of this
343 /// account type whose version byte is ≥ `min_version` and whose
344 /// data is at least as large as this layout.
345 ///
346 /// Use this during migration rollouts when a single instruction
347 /// must accept both old and new versions of an account.
348 ///
349 /// # Arguments
350 /// * `min_version`: lowest acceptable version byte (header byte 1).
351 /// Pass `1` to accept V1+, `2` to accept V2+ only, etc.
352 #[inline]
353 pub fn load_compatible<'a>(
354 account: &'a $crate::hopper_runtime::AccountView,
355 program_id: &$crate::hopper_runtime::Address,
356 min_version: u8,
357 ) -> Result<
358 $crate::hopper_core::account::VerifiedAccount<'a, Self>,
359 $crate::hopper_runtime::error::ProgramError,
360 > {
361 $crate::hopper_core::check::check_owner(account, program_id)?;
362 let data = account.try_borrow()?;
363 if data.len() < $crate::hopper_core::account::HEADER_LEN {
364 return Err($crate::hopper_runtime::error::ProgramError::AccountDataTooSmall);
365 }
366 // Check discriminator (same account type, any version).
367 if data[0] != Self::DISC {
368 return Err($crate::hopper_runtime::error::ProgramError::InvalidAccountData);
369 }
370 // Check minimum version.
371 if data[1] < min_version {
372 return Err($crate::hopper_runtime::error::ProgramError::InvalidAccountData);
373 }
374 // Minimum size (account may be larger if migrated to a newer version).
375 if data.len() < Self::LEN {
376 return Err($crate::hopper_runtime::error::ProgramError::AccountDataTooSmall);
377 }
378 $crate::hopper_core::account::VerifiedAccount::from_ref(data)
379 }
380
381 /// Tier 3m: Version-compatible load (mutable).
382 ///
383 /// Same as [`load_compatible`] but returns a mutable overlay.
384 #[inline]
385 pub fn load_compatible_mut<'a>(
386 account: &'a $crate::hopper_runtime::AccountView,
387 program_id: &$crate::hopper_runtime::Address,
388 min_version: u8,
389 ) -> Result<
390 $crate::hopper_core::account::VerifiedAccountMut<'a, Self>,
391 $crate::hopper_runtime::error::ProgramError,
392 > {
393 $crate::hopper_core::check::check_owner(account, program_id)?;
394 $crate::hopper_core::check::check_writable(account)?;
395 let data = account.try_borrow_mut()?;
396 if data.len() < $crate::hopper_core::account::HEADER_LEN {
397 return Err($crate::hopper_runtime::error::ProgramError::AccountDataTooSmall);
398 }
399 if data[0] != Self::DISC {
400 return Err($crate::hopper_runtime::error::ProgramError::InvalidAccountData);
401 }
402 if data[1] < min_version {
403 return Err($crate::hopper_runtime::error::ProgramError::InvalidAccountData);
404 }
405 if data.len() < Self::LEN {
406 return Err($crate::hopper_runtime::error::ProgramError::AccountDataTooSmall);
407 }
408 $crate::hopper_core::account::VerifiedAccountMut::from_ref_mut(data)
409 }
410
411 /// Tier 4: Unchecked load (caller assumes all risk).
412 ///
413 /// # Safety
414 /// Caller must guarantee the data is valid for this layout.
415 ///
416 /// **Deprecated:** Use `load()` for safe access. For explicit
417 /// unsafe access, use the raw byte pointer directly.
418 #[deprecated(since = "0.2.0", note = "use load() for safe access")]
419 #[inline(always)]
420 pub unsafe fn load_unchecked(data: &[u8]) -> &Self {
421 &*(data.as_ptr() as *const Self)
422 }
423
424 /// Write the header for a freshly initialized account.
425 #[inline(always)]
426 pub fn write_init_header(data: &mut [u8]) -> Result<(), $crate::hopper_runtime::error::ProgramError> {
427 $crate::hopper_core::account::write_header(
428 data,
429 Self::DISC,
430 Self::VERSION,
431 &Self::LAYOUT_ID,
432 )
433 }
434
435 // -- BUMP_OFFSET PDA Optimization ------
436 //
437 // Scans fields for a `bump` field. If found, generates
438 // BUMP_OFFSET const and verify_pda_cached() convenience method.
439 // Saves ~344 CU per PDA check vs find_program_address.
440
441 /// Byte offset of the bump field (if present). Used by BUMP_OFFSET PDA optimization.
442 /// If no bump field exists, this is set to usize::MAX as a sentinel.
443 pub const BUMP_OFFSET: usize = {
444 let mut offset = $crate::hopper_core::account::HEADER_LEN;
445 let mut found = usize::MAX;
446 $(
447 if $crate::hopper_core::__str_eq(stringify!($field), "bump") {
448 found = offset;
449 }
450 offset += $fsize;
451 )+
452 let _ = offset;
453 found
454 };
455
456 /// Returns `true` if this layout has a bump field for PDA optimization.
457 #[inline(always)]
458 pub const fn has_bump_offset() -> bool {
459 Self::BUMP_OFFSET != usize::MAX
460 }
461
462 /// Verify a PDA using the bump stored in account data (~200 CU).
463 ///
464 /// Reads the bump from `BUMP_OFFSET`, appends it to seeds, then
465 /// uses SHA-256 verify-only. Saves ~1300 CU vs `find_program_address`.
466 ///
467 /// Only available on layouts with a `bump` field. Panics at compile
468 /// time otherwise (asserts `BUMP_OFFSET != usize::MAX`).
469 #[inline]
470 pub fn verify_pda_cached(
471 account: &$crate::hopper_runtime::AccountView,
472 seeds: &[&[u8]],
473 program_id: &$crate::hopper_runtime::Address,
474 ) -> Result<(), $crate::hopper_runtime::error::ProgramError> {
475 // BUMP_OFFSET is a const, so this comparison is optimized away.
476 if Self::BUMP_OFFSET == usize::MAX {
477 return Err($crate::hopper_runtime::error::ProgramError::InvalidArgument);
478 }
479 $crate::hopper_runtime::pda::verify_pda_from_stored_bump(
480 account, seeds, Self::BUMP_OFFSET, program_id,
481 )
482 }
483
484 // -- Tier 5: Unverified Overlay ------
485 //
486 // Best-effort loading for indexers and off-chain tooling.
487 // Attempts header validation but returns the overlay even on
488 // failure, with a bool indicating whether validation passed.
489
490 /// Tier 5: Unverified overlay for indexers/tooling.
491 ///
492 /// Attempts to validate the header but returns the overlay
493 /// regardless. The returned bool is `true` if validation passed.
494 ///
495 /// This is safe to call on any data -- it never panics.
496 #[inline]
497 pub fn load_unverified(data: &[u8]) -> Option<(&Self, bool)> {
498 if data.len() < Self::LEN {
499 return None;
500 }
501 let validated = $crate::hopper_core::account::check_header(
502 data,
503 Self::DISC,
504 Self::VERSION,
505 &Self::LAYOUT_ID,
506 )
507 .is_ok();
508 // SAFETY: Size checked above. T: Pod, alignment-1.
509 let overlay = unsafe { &*(data.as_ptr() as *const Self) };
510 Some((overlay, validated))
511 }
512
513 // -- Multi-Owner Foreign Load ------
514 //
515 // Load foreign account that could be owned by any of several
516 // programs (e.g., Token vs Token-2022).
517
518 /// Tier 2m: Foreign load with multiple possible owners.
519 ///
520 /// Returns `(VerifiedAccount, owner_index)` where `owner_index`
521 /// indicates which owner matched.
522 #[inline]
523 pub fn load_foreign_multi<'a>(
524 account: &'a $crate::hopper_runtime::AccountView,
525 owners: &[&$crate::hopper_runtime::Address],
526 ) -> Result<
527 ($crate::hopper_core::account::VerifiedAccount<'a, Self>, usize),
528 $crate::hopper_runtime::error::ProgramError,
529 > {
530 let owner_idx = $crate::hopper_core::check::check_owner_multi(account, owners)?;
531 let data = account.try_borrow()?;
532 let layout_id = $crate::hopper_core::account::read_layout_id(&*data)?;
533 if layout_id != Self::LAYOUT_ID {
534 return Err($crate::hopper_runtime::error::ProgramError::InvalidAccountData);
535 }
536 $crate::hopper_core::check::check_size(&*data, Self::LEN)?;
537 let verified = $crate::hopper_core::account::VerifiedAccount::from_ref(data)?;
538 Ok((verified, owner_idx))
539 }
540 }
541
542 // Implement HopperLayout for modifier-style wrappers.
543 impl $crate::hopper_core::check::modifier::HopperLayout for $name {
544 const DISC: u8 = $disc;
545 const VERSION: u8 = $ver;
546 const LAYOUT_ID: [u8; 8] = $name::LAYOUT_ID;
547 const LEN_WITH_HEADER: usize = $name::LEN;
548 }
549 };
550}
551
552// ═════════════════════════════════════════════════════════════════════
553// Section: Validation (check / error / require)
554// ═════════════════════════════════════════════════════════════════════
555
556/// Composable account constraint checking.
557///
558/// ```ignore
559/// hopper_check!(vault,
560/// owner = program_id,
561/// writable,
562/// signer,
563/// disc = Vault::DISC,
564/// size >= Vault::LEN,
565/// );
566/// ```
567#[macro_export]
568macro_rules! hopper_check {
569 ($account:expr, $( $constraint:tt )+) => {{
570 $crate::_hopper_check_inner!($account, $( $constraint )+)
571 }};
572}
573
574// Internal constraint dispatcher
575#[doc(hidden)]
576#[macro_export]
577macro_rules! _hopper_check_inner {
578 // owner = $id
579 ($account:expr, owner = $id:expr $(, $($rest:tt)+ )?) => {{
580 $crate::hopper_core::check::check_owner($account, $id)?;
581 $( $crate::_hopper_check_inner!($account, $($rest)+); )?
582 }};
583 // writable
584 ($account:expr, writable $(, $($rest:tt)+ )?) => {{
585 $crate::hopper_core::check::check_writable($account)?;
586 $( $crate::_hopper_check_inner!($account, $($rest)+); )?
587 }};
588 // signer
589 ($account:expr, signer $(, $($rest:tt)+ )?) => {{
590 $crate::hopper_core::check::check_signer($account)?;
591 $( $crate::_hopper_check_inner!($account, $($rest)+); )?
592 }};
593 // disc = $d
594 ($account:expr, disc = $d:expr $(, $($rest:tt)+ )?) => {{
595 let data = $account.try_borrow()?;
596 $crate::hopper_core::check::check_discriminator(&*data, $d)?;
597 $( $crate::_hopper_check_inner!($account, $($rest)+); )?
598 }};
599 // size >= $n
600 ($account:expr, size >= $n:expr $(, $($rest:tt)+ )?) => {{
601 let data = $account.try_borrow()?;
602 $crate::hopper_core::check::check_size(&*data, $n)?;
603 $( $crate::_hopper_check_inner!($account, $($rest)+); )?
604 }};
605 // Base case
606 ($account:expr,) => {};
607}
608
609/// Generate sequential error codes.
610///
611/// ```ignore
612/// hopper_error! {
613/// base = 6000;
614/// Undercollateralized, // 6000
615/// Expired, // 6001
616/// InvalidOracle, // 6002
617/// }
618/// ```
619#[macro_export]
620macro_rules! hopper_error {
621 (
622 base = $base:literal;
623 $( $name:ident ),+ $(,)?
624 ) => {
625 $crate::_hopper_error_inner!($base; $( $name ),+);
626 };
627}
628
629#[doc(hidden)]
630#[macro_export]
631macro_rules! _hopper_error_inner {
632 // Base case: single ident
633 ($code:expr; $name:ident) => {
634 pub struct $name;
635 impl $name {
636 pub const CODE: u32 = $code;
637 }
638 impl From<$name> for $crate::hopper_runtime::error::ProgramError {
639 fn from(_: $name) -> $crate::hopper_runtime::error::ProgramError {
640 $crate::hopper_runtime::error::ProgramError::Custom($code)
641 }
642 }
643 };
644 // Recursive case: first ident + rest
645 ($code:expr; $name:ident, $($rest:ident),+) => {
646 $crate::_hopper_error_inner!($code; $name);
647 $crate::_hopper_error_inner!($code + 1; $($rest),+);
648 };
649}
650
651/// Require a condition, returning a custom error if false.
652///
653/// ```ignore
654/// hopper_require!(amount > 0, ZeroAmount)?;
655/// ```
656#[macro_export]
657macro_rules! hopper_require {
658 ($cond:expr, $err:expr) => {
659 if !$cond {
660 return Err($err.into());
661 }
662 };
663}
664
665// ═════════════════════════════════════════════════════════════════════
666// Section: Lifecycle (init / close)
667// ═════════════════════════════════════════════════════════════════════
668
669/// Initialize an account: allocate, assign, zero-init, write header.
670///
671/// ```ignore
672/// hopper_init!(payer, account, system_program, program_id, Vault)?;
673/// ```
674#[macro_export]
675macro_rules! hopper_init {
676 ($payer:expr, $account:expr, $system:expr, $program_id:expr, $layout:ty) => {{
677 let payer = $payer;
678 let account = $account;
679 let program_id = $program_id;
680
681 let lamports = $crate::hopper_core::check::rent_exempt_min(<$layout>::LEN);
682 let space = <$layout>::LEN as u64;
683
684 if account.data_len() != 0 {
685 Err($crate::hopper_runtime::ProgramError::AccountAlreadyInitialized)?;
686 }
687
688 let current_lamports = account.lamports();
689 if current_lamports == 0 {
690 $crate::hopper_system::CreateAccount {
691 from: payer,
692 to: account,
693 lamports,
694 space,
695 owner: program_id,
696 }
697 .invoke()?;
698 } else {
699 if current_lamports < lamports {
700 $crate::hopper_system::Transfer {
701 from: payer,
702 to: account,
703 lamports: lamports - current_lamports,
704 }
705 .invoke()?;
706 }
707 $crate::hopper_system::Allocate { account, space }.invoke()?;
708 $crate::hopper_system::Assign {
709 account,
710 owner: program_id,
711 }
712 .invoke()?;
713 }
714
715 let mut data = account.try_borrow_mut()?;
716 $crate::hopper_core::account::zero_init(&mut *data);
717 <$layout>::write_init_header(&mut *data)
718 }};
719}
720
721/// Safely close an account with sentinel protection.
722///
723/// ```ignore
724/// hopper_close!(account, destination)?;
725/// ```
726#[macro_export]
727macro_rules! hopper_close {
728 ($account:expr, $destination:expr) => {
729 $crate::hopper_core::account::safe_close_with_sentinel($account, $destination)
730 };
731}
732
733// ═════════════════════════════════════════════════════════════════════
734// Section: Dispatch (discriminator registry)
735// ═════════════════════════════════════════════════════════════════════
736
737/// Discriminator registry -- compile-time uniqueness enforcement.
738///
739/// Lists all account types for a program and asserts that no two share
740/// a discriminator. This prevents silent bugs where `Vault::load()` could
741/// accidentally succeed on a `Pool` account.
742///
743/// ```ignore
744/// hopper_register_discs! {
745/// Vault,
746/// Pool,
747/// Position,
748/// }
749/// ```
750///
751/// Fails at compile time if any two types share the same DISC value.
752#[macro_export]
753macro_rules! hopper_register_discs {
754 ( $( $layout:ty ),+ $(,)? ) => {
755 const _: () = {
756 let discs: &[u8] = &[ $( <$layout>::DISC, )+ ];
757 let names: &[&str] = &[ $( stringify!($layout), )+ ];
758 let n = discs.len();
759 let mut i = 0;
760 while i < n {
761 let mut j = i + 1;
762 while j < n {
763 assert!(
764 discs[i] != discs[j],
765 // Can't format at const time, but this gives a clear enough message
766 "Duplicate discriminator detected in hopper_register_discs!"
767 );
768 j += 1;
769 }
770 i += 1;
771 }
772 let _ = names; // consumed for error messages in non-const contexts
773 };
774 };
775}
776
777// ═════════════════════════════════════════════════════════════════════
778// Section: PDA
779// ═════════════════════════════════════════════════════════════════════
780
781/// PDA verification with BUMP_OFFSET optimization.
782///
783/// If the layout has a bump field, reads bump from account data and uses
784/// `create_program_address` (~200 CU). Otherwise falls back to
785/// `find_program_address` (~544 CU).
786///
787/// ```ignore
788/// hopper_verify_pda!(vault_account, &[b"vault", authority.as_ref()], program_id, Vault)?;
789/// ```
790#[macro_export]
791macro_rules! hopper_verify_pda {
792 ($account:expr, $seeds:expr, $program_id:expr, $layout:ty) => {{
793 if <$layout>::has_bump_offset() {
794 $crate::hopper_core::check::verify_pda_cached(
795 $account,
796 $seeds,
797 <$layout>::BUMP_OFFSET,
798 $program_id,
799 )
800 } else {
801 // Fallback: no bump field, use regular verify
802 match $crate::hopper_core::check::find_and_verify_pda($account, $seeds, $program_id) {
803 Ok(_bump) => Ok(()),
804 Err(e) => Err(e),
805 }
806 }
807 }};
808}
809
810// ═════════════════════════════════════════════════════════════════════
811// Section: Invariants
812// ═════════════════════════════════════════════════════════════════════
813
814/// Invariant checking macro.
815///
816/// Defines a set of invariants for an instruction that run after mutation.
817/// Each invariant is a closure over account data that returns `ProgramResult`.
818///
819/// ```ignore
820/// hopper_invariant! {
821/// "balance_conserved" => |vault: &Vault| {
822/// let bal = vault.balance.get();
823/// hopper_require!(bal <= MAX_SUPPLY, BalanceOverflow);
824/// Ok(())
825/// },
826/// "authority_unchanged" => |vault: &Vault, old: &Vault| {
827/// hopper_require!(vault.authority == old.authority, AuthorityChanged);
828/// Ok(())
829/// },
830/// }
831/// ```
832///
833/// Generates an inline invariant runner that returns the first failure.
834#[macro_export]
835macro_rules! hopper_invariant {
836 ( $( $label:literal => $check:expr ),+ $(,)? ) => {{
837 let mut _result: $crate::hopper_runtime::ProgramResult = Ok(());
838 $(
839 if _result.is_ok() {
840 _result = $check;
841 }
842 )+
843 _result
844 }};
845}
846
847// ═════════════════════════════════════════════════════════════════════
848// Section: Manifest (schema export)
849// ═════════════════════════════════════════════════════════════════════
850
851/// Generate a layout manifest for schema tooling.
852///
853/// Produces a `const LayoutManifest` for a layout type, with field
854/// descriptors suitable for off-chain tooling, indexers, and migration
855/// compatibility checks.
856///
857/// ```ignore
858/// hopper_manifest! {
859/// VAULT_MANIFEST = Vault {
860/// authority: [u8; 32] = 32,
861/// mint: [u8; 32] = 32,
862/// balance: WireU64 = 8,
863/// bump: u8 = 1,
864/// }
865/// }
866/// ```
867///
868/// Generates: `pub const VAULT_MANIFEST: hopper_schema::LayoutManifest`
869#[macro_export]
870macro_rules! hopper_manifest {
871 (
872 $const_name:ident = $name:ident {
873 $( $field:ident : $fty:ty = $fsize:literal ),+ $(,)?
874 }
875 ) => {
876 pub const $const_name: $crate::hopper_schema::LayoutManifest = {
877 const FIELD_COUNT: usize = 0 $( + { let _ = stringify!($field); 1 } )+;
878 const SIZES: [u16; FIELD_COUNT] = [ $( $fsize ),+ ];
879 const NAMES: [&str; FIELD_COUNT] = [ $( stringify!($field) ),+ ];
880 const TYPES: [&str; FIELD_COUNT] = [ $( stringify!($fty) ),+ ];
881 const FIELDS: [$crate::hopper_schema::FieldDescriptor; FIELD_COUNT] = {
882 let h = $crate::hopper_core::account::HEADER_LEN as u16;
883 let mut result = [$crate::hopper_schema::FieldDescriptor {
884 name: "", canonical_type: "", size: 0, offset: 0,
885 intent: $crate::hopper_schema::FieldIntent::Custom,
886 }; FIELD_COUNT];
887 let mut offset = h;
888 let mut i = 0;
889 while i < FIELD_COUNT {
890 result[i] = $crate::hopper_schema::FieldDescriptor {
891 name: NAMES[i],
892 canonical_type: TYPES[i],
893 size: SIZES[i],
894 offset,
895 intent: $crate::hopper_schema::FieldIntent::Custom,
896 };
897 offset += SIZES[i];
898 i += 1;
899 }
900 result
901 };
902 $crate::hopper_schema::LayoutManifest {
903 name: stringify!($name),
904 version: <$name>::VERSION,
905 disc: <$name>::DISC,
906 layout_id: <$name>::LAYOUT_ID,
907 total_size: <$name>::LEN,
908 field_count: FIELD_COUNT,
909 fields: &FIELDS,
910 }
911 };
912 };
913}
914
915// ═════════════════════════════════════════════════════════════════════
916// Section: Segmented accounts
917// ═════════════════════════════════════════════════════════════════════
918
919/// Declare a segmented account with typed segments.
920///
921/// Generates:
922/// - Segment ID constants (FNV-1a)
923/// - A `register_segments` function that initializes the segment registry
924/// - Per-segment accessor methods on a generated context struct
925///
926/// ```ignore
927/// hopper_segment! {
928/// pub struct Treasury, disc = 3 {
929/// core: TreasuryCore = 128,
930/// permissions: PermissionsTable = 256,
931/// history: HistoryLog = 512,
932/// }
933/// }
934///
935/// // Initialize:
936/// Treasury::init_segments(data)?;
937///
938/// // Read:
939/// let core: &TreasuryCore = Treasury::load_segment::<TreasuryCore>(data, Treasury::CORE_ID)?;
940/// ```
941#[macro_export]
942macro_rules! hopper_segment {
943 (
944 $(#[$attr:meta])*
945 pub struct $name:ident, disc = $disc:literal
946 {
947 $( $seg:ident : $sty:ty = $ssize:literal ),+ $(,)?
948 }
949 ) => {
950 $(#[$attr])*
951 pub struct $name;
952
953 impl $name {
954 pub const DISC: u8 = $disc;
955
956 // Generate segment ID constants
957 $crate::_hopper_segment_ids!($( $seg ),+);
958
959 // Segment count
960 pub const SEGMENT_COUNT: usize = $crate::_hopper_segment_count!($( $seg ),+);
961
962 /// Total account size: header + registry header + entries + segment data.
963 pub const TOTAL_SIZE: usize = {
964 let registry_size = $crate::hopper_core::account::registry::REGISTRY_HEADER_SIZE
965 + (Self::SEGMENT_COUNT * $crate::hopper_core::account::registry::SEGMENT_ENTRY_SIZE);
966 $crate::hopper_core::account::HEADER_LEN
967 + registry_size
968 $( + $ssize )+
969 };
970
971 /// Initialize the segment registry with all declared segments.
972 #[inline]
973 pub fn init_segments(data: &mut [u8]) -> Result<(), $crate::hopper_runtime::error::ProgramError> {
974 let specs: &[($crate::hopper_core::account::registry::SegmentId, u32, u8)] = &[
975 $(
976 (
977 $crate::hopper_core::account::registry::segment_id(stringify!($seg)),
978 $ssize as u32,
979 1u8,
980 ),
981 )+
982 ];
983 $crate::hopper_core::account::SegmentRegistryMut::init(data, specs)
984 }
985
986 /// Load a typed overlay from a named segment (immutable).
987 #[inline]
988 pub fn load_segment<T: $crate::hopper_core::account::Pod + $crate::hopper_core::account::FixedLayout>(
989 data: &[u8],
990 seg_id: &$crate::hopper_core::account::registry::SegmentId,
991 ) -> Result<&T, $crate::hopper_runtime::error::ProgramError> {
992 let registry = $crate::hopper_core::account::SegmentRegistry::from_account(data)?;
993 registry.segment_overlay::<T>(seg_id)
994 }
995
996 /// Load a typed overlay from a named segment (mutable).
997 #[inline]
998 pub fn load_segment_mut<T: $crate::hopper_core::account::Pod + $crate::hopper_core::account::FixedLayout>(
999 data: &mut [u8],
1000 seg_id: &$crate::hopper_core::account::registry::SegmentId,
1001 ) -> Result<&mut T, $crate::hopper_runtime::error::ProgramError> {
1002 let mut registry = $crate::hopper_core::account::SegmentRegistryMut::from_account_mut(data)?;
1003 registry.segment_overlay_mut::<T>(seg_id)
1004 }
1005 }
1006 };
1007}
1008
1009/// Generate uppercase segment ID constants from field names.
1010#[doc(hidden)]
1011#[macro_export]
1012macro_rules! _hopper_segment_ids {
1013 ( $( $seg:ident ),+ ) => {
1014 $(
1015 // Use paste-style approach: just use the name directly as a const
1016 // The user references it as TypeName::SEGNAME_ID
1017 $crate::_hopper_segment_id_const!($seg);
1018 )+
1019 };
1020}
1021
1022/// Generate a single segment ID constant.
1023///
1024/// Produces `pub const {NAME}_ID: SegmentId = segment_id("name");`
1025/// Due to macro_rules limitations we use the exact field name in
1026/// uppercase manually. This generates as-is with _ID suffix.
1027#[doc(hidden)]
1028#[macro_export]
1029macro_rules! _hopper_segment_id_const {
1030 ($seg:ident) => {
1031 #[doc = concat!("Segment ID for `", stringify!($seg), "`.")]
1032 #[allow(non_upper_case_globals)]
1033 pub const $seg: $crate::hopper_core::account::registry::SegmentId =
1034 $crate::hopper_core::account::registry::segment_id(stringify!($seg));
1035 };
1036}
1037
1038/// Count segments.
1039#[doc(hidden)]
1040#[macro_export]
1041macro_rules! _hopper_segment_count {
1042 ( $( $seg:ident ),+ ) => {
1043 {
1044 let mut _n = 0usize;
1045 $( let _ = stringify!($seg); _n += 1; )+
1046 _n
1047 }
1048 };
1049}
1050
1051// ═════════════════════════════════════════════════════════════════════
1052// Section: Validation pipeline builder
1053// ═════════════════════════════════════════════════════════════════════
1054
1055/// Build a validation pipeline declaratively.
1056///
1057/// Each rule is a combinator that returns `impl Fn(&ValidationContext) -> ProgramResult`.
1058/// The macro creates a context, enforces unique writable accounts by default,
1059/// and then invokes each rule in order (fail-fast).
1060#[macro_export]
1061macro_rules! hopper_validate {
1062 (
1063 accounts = $accounts:expr,
1064 program_id = $program_id:expr,
1065 data = $data:expr,
1066 rules {
1067 $( $rule:expr ),+ $(,)?
1068 }
1069 ) => {{
1070 let _vctx = $crate::hopper_core::check::graph::ValidationContext::new(
1071 $program_id,
1072 $accounts,
1073 $data,
1074 );
1075 $crate::hopper_core::check::graph::require_unique_writable_accounts()(&_vctx)?;
1076 $( ($rule)(&_vctx)?; )+
1077 Ok::<(), $crate::hopper_runtime::error::ProgramError>(())
1078 }};
1079}
1080
1081// ═════════════════════════════════════════════════════════════════════
1082// Section: Virtual state (multi-account mapping)
1083// ═════════════════════════════════════════════════════════════════════
1084
1085/// Declare a multi-account virtual state mapping.
1086///
1087/// ```ignore
1088/// let market = hopper_virtual! {
1089/// slots = 3,
1090/// map {
1091/// 0 => account_index: 1, owned, writable,
1092/// 1 => account_index: 2, owned,
1093/// 2 => account_index: 3,
1094/// }
1095/// };
1096///
1097/// market.validate(accounts, program_id)?;
1098/// let core: &MarketCore = market.overlay::<MarketCore>(accounts, 0)?;
1099/// ```
1100#[macro_export]
1101macro_rules! hopper_virtual {
1102 (
1103 slots = $n:literal,
1104 map {
1105 $( $slot:literal => account_index: $idx:literal
1106 $(, owned $( = $owner:expr )? )?
1107 $(, writable )?
1108 ),+ $(,)?
1109 }
1110 ) => {{
1111 let mut vs = $crate::hopper_core::virtual_state::VirtualState::<$n>::new();
1112 $(
1113 vs = $crate::_hopper_virtual_slot!(vs, $slot, $idx
1114 $(, owned $( = $owner )? )?
1115 $(, writable )?
1116 );
1117 )+
1118 vs
1119 }};
1120}
1121
1122/// Apply a single virtual slot mapping.
1123#[doc(hidden)]
1124#[macro_export]
1125macro_rules! _hopper_virtual_slot {
1126 // owned + writable
1127 ($vs:expr, $slot:literal, $idx:literal, owned, writable) => {
1128 $vs.map_mut($slot, $idx)
1129 };
1130 // owned only
1131 ($vs:expr, $slot:literal, $idx:literal, owned) => {
1132 $vs.map($slot, $idx)
1133 };
1134 // writable only (no owner check)
1135 ($vs:expr, $slot:literal, $idx:literal, writable) => {
1136 $vs.set_slot(
1137 $slot,
1138 $crate::hopper_core::virtual_state::VirtualSlot {
1139 account_index: $idx,
1140 require_owned: false,
1141 require_writable: true,
1142 },
1143 )
1144 };
1145 // bare (no constraints)
1146 ($vs:expr, $slot:literal, $idx:literal) => {
1147 $vs.map_foreign($slot, $idx)
1148 };
1149}
1150
1151// ═════════════════════════════════════════════════════════════════════
1152// Section: Compile-time compatibility & ABI assertions
1153// ═════════════════════════════════════════════════════════════════════
1154
1155/// Assert that two layout versions have compatible fingerprints.
1156///
1157/// Fails at compile time if the assertion doesn't hold.
1158/// Use this in tests and CI to catch accidental schema breaks.
1159///
1160/// ```ignore
1161/// // Assert V2 is a strict superset of V1 (append-only):
1162/// hopper_assert_compatible!(VaultV1, VaultV2, append);
1163///
1164/// // Assert two layouts have different fingerprints (version bump required):
1165/// hopper_assert_compatible!(VaultV1, VaultV2, differs);
1166/// ```
1167#[macro_export]
1168macro_rules! hopper_assert_compatible {
1169 // Assert V2 is append-compatible with V1: different fingerprint + larger size + same disc
1170 ($old:ty, $new:ty, append) => {
1171 const _: () = {
1172 assert!(
1173 <$new>::LEN > <$old>::LEN,
1174 "New layout must be larger than old for append compatibility"
1175 );
1176 assert!(
1177 <$new>::DISC == <$old>::DISC,
1178 "Discriminator must remain the same across versions"
1179 );
1180 assert!(
1181 <$new>::VERSION > <$old>::VERSION,
1182 "New version must be strictly greater"
1183 );
1184 // Layout IDs must differ (field set changed)
1185 let old_id = <$old>::LAYOUT_ID;
1186 let new_id = <$new>::LAYOUT_ID;
1187 let mut same = true;
1188 let mut i = 0;
1189 while i < 8 {
1190 if old_id[i] != new_id[i] {
1191 same = false;
1192 }
1193 i += 1;
1194 }
1195 assert!(!same, "Layout IDs must differ between versions");
1196 };
1197 };
1198 // Assert two layouts have different fingerprints
1199 ($old:ty, $new:ty, differs) => {
1200 const _: () = {
1201 let old_id = <$old>::LAYOUT_ID;
1202 let new_id = <$new>::LAYOUT_ID;
1203 let mut same = true;
1204 let mut i = 0;
1205 while i < 8 {
1206 if old_id[i] != new_id[i] {
1207 same = false;
1208 }
1209 i += 1;
1210 }
1211 assert!(!same, "Layout IDs must differ between versions");
1212 };
1213 };
1214}
1215
1216/// Assert that a layout's fingerprint matches an expected value.
1217///
1218/// Use this to pin a layout's fingerprint in tests. If someone changes the
1219/// layout fields, this assertion catches the ABI break at compile time.
1220///
1221/// ```ignore
1222/// hopper_assert_fingerprint!(Vault, [0x1a, 0x2b, 0x3c, 0x4d, 0x5e, 0x6f, 0x70, 0x81]);
1223/// ```
1224#[macro_export]
1225macro_rules! hopper_assert_fingerprint {
1226 ($layout:ty, $expected:expr) => {
1227 const _: () = {
1228 let actual = <$layout>::LAYOUT_ID;
1229 let expected: [u8; 8] = $expected;
1230 let mut i = 0;
1231 while i < 8 {
1232 assert!(
1233 actual[i] == expected[i],
1234 "Layout fingerprint doesn't match expected value -- ABI may have changed"
1235 );
1236 i += 1;
1237 }
1238 };
1239 };
1240}
1241
1242// Re-export dispatch from core
1243pub use hopper_core;
1244pub use hopper_runtime;
1245pub use hopper_schema;
1246pub use hopper_system;
1247
1248/// Compile-time assertion for safe manual `Pod` implementations.
1249///
1250/// Verifies that a type meets all Pod requirements:
1251/// - `align_of == 1` (required for zero-copy overlay at any offset)
1252/// - `size_of == SIZE` matches declared SIZE
1253///
1254/// Use this when implementing `Pod` manually (outside of `hopper_layout!`).
1255///
1256/// ```ignore
1257/// #[repr(C)]
1258/// #[derive(Clone, Copy)]
1259/// pub struct MyEntry {
1260/// pub key: [u8; 32],
1261/// pub value: WireU64,
1262/// }
1263///
1264/// const_assert_pod!(MyEntry, 40);
1265/// unsafe impl Pod for MyEntry {}
1266/// ```
1267#[macro_export]
1268macro_rules! const_assert_pod {
1269 ($ty:ty, $size:expr) => {
1270 const _: () = assert!(
1271 core::mem::align_of::<$ty>() == 1,
1272 concat!(
1273 "Pod type `",
1274 stringify!($ty),
1275 "` must have alignment 1 for zero-copy safety. ",
1276 "Ensure all fields use alignment-1 wire types ([u8; N], WireU64, etc.)."
1277 )
1278 );
1279 const _: () = assert!(
1280 core::mem::size_of::<$ty>() == $size,
1281 concat!(
1282 "Pod type `",
1283 stringify!($ty),
1284 "` size mismatch: ",
1285 "expected ",
1286 stringify!($size),
1287 " bytes"
1288 )
1289 );
1290 };
1291}
1292
1293// ═════════════════════════════════════════════════════════════════════
1294// Section: Cross-program interface
1295// ═════════════════════════════════════════════════════════════════════
1296
1297/// Declare a cross-program interface view.
1298///
1299/// Generates a read-only overlay struct for reading accounts owned by
1300/// another program **without any crate dependency**. The interface is
1301/// pinned by `LAYOUT_ID` -- the same deterministic SHA-256 fingerprint
1302/// used by `hopper_layout!`. If the originating program changes its
1303/// layout, the fingerprint will differ and `load_foreign()` will reject
1304/// the account at runtime.
1305///
1306/// The generated struct includes:
1307/// - `#[repr(C)]` zero-copy overlay with alignment-1 guarantee
1308/// - Deterministic `LAYOUT_ID` matching the originating layout
1309/// - `load_foreign(account, expected_owner)` for Tier-2 cross-program reads
1310/// - `load_foreign_multi(account, owners)` for multi-owner scenarios
1311/// - `load_with_profile(account, TrustProfile)` for configurable trust
1312/// - Compile-time size and alignment assertions
1313///
1314/// # Example
1315///
1316/// Program A defines a Vault:
1317/// ```ignore
1318/// hopper_layout! {
1319/// pub struct Vault, disc = 1, version = 1 {
1320/// authority: TypedAddress<Authority> = 32,
1321/// balance: WireU64 = 8,
1322/// bump: u8 = 1,
1323/// }
1324/// }
1325/// ```
1326///
1327/// Program B reads it **without importing Program A**:
1328/// ```ignore
1329/// hopper_interface! {
1330/// /// Read-only view of Program A's Vault.
1331/// pub struct VaultView, disc = 1, version = 1 {
1332/// authority: TypedAddress<Authority> = 32,
1333/// balance: WireU64 = 8,
1334/// bump: u8 = 1,
1335/// }
1336/// }
1337///
1338/// let verified = VaultView::load_foreign(vault_account, &PROGRAM_A_ID)?;
1339/// let balance = verified.get().balance.get();
1340/// ```
1341///
1342/// If the fields match Program A's Vault exactly, the LAYOUT_IDs will
1343/// be identical and `load_foreign` succeeds. Any structural divergence
1344/// produces a different hash and the load fails.
1345#[macro_export]
1346macro_rules! hopper_interface {
1347 (
1348 $(#[$attr:meta])*
1349 pub struct $name:ident, disc = $disc:literal, version = $ver:literal
1350 {
1351 $(
1352 $(#[$field_attr:meta])*
1353 $field:ident : $fty:ty = $fsize:literal
1354 ),+ $(,)?
1355 }
1356 ) => {
1357 $(#[$attr])*
1358 #[derive(Clone, Copy)]
1359 #[repr(C)]
1360 pub struct $name {
1361 pub header: $crate::hopper_core::account::AccountHeader,
1362 $(
1363 $(#[$field_attr])*
1364 pub $field: $fty,
1365 )+
1366 }
1367
1368 // Compile-time assertions
1369 const _: () = {
1370 let expected = $crate::hopper_core::account::HEADER_LEN $( + $fsize )+;
1371 assert!(
1372 core::mem::size_of::<$name>() == expected,
1373 "Interface size mismatch: struct size != declared field sizes + header"
1374 );
1375 assert!(
1376 core::mem::align_of::<$name>() == 1,
1377 "Interface alignment must be 1 for zero-copy safety"
1378 );
1379 };
1380
1381 // Bytemuck proof (Hopper Safety Audit Must-Fix #5), same
1382 // justification as `hopper_layout!`: `#[repr(C)]` over Hopper
1383 // wire types is bytemuck-safe by construction.
1384 #[cfg(feature = "hopper-native-backend")]
1385 unsafe impl $crate::hopper_runtime::__hopper_native::bytemuck::Zeroable for $name {}
1386 #[cfg(feature = "hopper-native-backend")]
1387 unsafe impl $crate::hopper_runtime::__hopper_native::bytemuck::Pod for $name {}
1388
1389 // SAFETY: #[repr(C)] over alignment-1 fields, all bit patterns valid.
1390 unsafe impl $crate::hopper_core::account::Pod for $name {}
1391
1392 // Audit final-API Step 5 seal (second declarative-macro form).
1393 unsafe impl $crate::hopper_runtime::__sealed::HopperZeroCopySealed for $name {}
1394
1395 impl $crate::hopper_core::account::FixedLayout for $name {
1396 const SIZE: usize = $crate::hopper_core::account::HEADER_LEN $( + $fsize )+;
1397 }
1398
1399 impl $crate::hopper_core::field_map::FieldMap for $name {
1400 const FIELDS: &'static [$crate::hopper_core::field_map::FieldInfo] = {
1401 const FIELD_COUNT: usize = 0 $( + { let _ = stringify!($field); 1 } )+;
1402 const NAMES: [&str; FIELD_COUNT] = [ $( stringify!($field) ),+ ];
1403 const SIZES: [usize; FIELD_COUNT] = [ $( $fsize ),+ ];
1404 const FIELDS: [$crate::hopper_core::field_map::FieldInfo; FIELD_COUNT] = {
1405 let mut result = [$crate::hopper_core::field_map::FieldInfo::new("", 0, 0); FIELD_COUNT];
1406 let mut offset = $crate::hopper_core::account::HEADER_LEN;
1407 let mut index = 0;
1408 while index < FIELD_COUNT {
1409 result[index] = $crate::hopper_core::field_map::FieldInfo::new(
1410 NAMES[index],
1411 offset,
1412 SIZES[index],
1413 );
1414 offset += SIZES[index];
1415 index += 1;
1416 }
1417 result
1418 };
1419 &FIELDS
1420 };
1421 }
1422
1423 impl $crate::hopper_runtime::LayoutContract for $name {
1424 const DISC: u8 = $disc;
1425 const VERSION: u8 = $ver;
1426 const LAYOUT_ID: [u8; 8] = $name::LAYOUT_ID;
1427 const SIZE: usize = $name::LEN;
1428 const TYPE_OFFSET: usize = 0;
1429 }
1430
1431 impl $crate::hopper_schema::SchemaExport for $name {
1432 fn layout_manifest() -> $crate::hopper_schema::LayoutManifest {
1433 const FIELD_COUNT: usize = 0 $( + { let _ = stringify!($field); 1 } )+;
1434 const SIZES: [u16; FIELD_COUNT] = [ $( $fsize ),+ ];
1435 const NAMES: [&str; FIELD_COUNT] = [ $( stringify!($field) ),+ ];
1436 const TYPES: [&str; FIELD_COUNT] = [ $( stringify!($fty) ),+ ];
1437 const FIELDS: [$crate::hopper_schema::FieldDescriptor; FIELD_COUNT] = {
1438 let mut result = [$crate::hopper_schema::FieldDescriptor {
1439 name: "", canonical_type: "", size: 0, offset: 0,
1440 intent: $crate::hopper_schema::FieldIntent::Custom,
1441 }; FIELD_COUNT];
1442 let mut offset = $crate::hopper_core::account::HEADER_LEN as u16;
1443 let mut index = 0;
1444 while index < FIELD_COUNT {
1445 result[index] = $crate::hopper_schema::FieldDescriptor {
1446 name: NAMES[index],
1447 canonical_type: TYPES[index],
1448 size: SIZES[index],
1449 offset,
1450 intent: $crate::hopper_schema::FieldIntent::Custom,
1451 };
1452 offset += SIZES[index];
1453 index += 1;
1454 }
1455 result
1456 };
1457 $crate::hopper_schema::LayoutManifest {
1458 name: stringify!($name),
1459 version: <$name>::VERSION,
1460 disc: <$name>::DISC,
1461 layout_id: <$name>::LAYOUT_ID,
1462 total_size: <$name>::LEN,
1463 field_count: FIELD_COUNT,
1464 fields: &FIELDS,
1465 }
1466 }
1467 }
1468
1469 impl $name {
1470 /// Total byte size of this interface view.
1471 pub const LEN: usize = $crate::hopper_core::account::HEADER_LEN $( + $fsize )+;
1472
1473 /// Expected discriminator of the originating layout.
1474 pub const DISC: u8 = $disc;
1475
1476 /// Expected version of the originating layout.
1477 pub const VERSION: u8 = $ver;
1478
1479 /// Deterministic layout fingerprint.
1480 ///
1481 /// Matches the originating layout's `LAYOUT_ID` if the field
1482 /// names, types, sizes, and ordering are identical.
1483 pub const LAYOUT_ID: [u8; 8] = {
1484 const INPUT: &str = concat!(
1485 "hopper:v1:",
1486 stringify!($name), ":",
1487 stringify!($ver), ":",
1488 $( stringify!($field), ":", stringify!($fty), ":", stringify!($fsize), ",", )+
1489 );
1490 const HASH: [u8; 32] = $crate::hopper_core::__sha256_const(INPUT.as_bytes());
1491 [
1492 HASH[0], HASH[1], HASH[2], HASH[3],
1493 HASH[4], HASH[5], HASH[6], HASH[7],
1494 ]
1495 };
1496
1497 /// Read-only overlay (immutable).
1498 #[inline(always)]
1499 pub fn overlay(data: &[u8]) -> Result<&Self, $crate::hopper_runtime::error::ProgramError> {
1500 $crate::hopper_core::account::pod_from_bytes::<Self>(data)
1501 }
1502
1503 /// Tier 2: Cross-program foreign load (read-only).
1504 ///
1505 /// Validates: owner + layout_id + exact size.
1506 /// No discriminator or version check -- the layout_id is the ABI proof.
1507 ///
1508 /// **Deprecated:** Renamed to `load_cross_program()` for clarity.
1509 #[deprecated(since = "0.2.0", note = "renamed to load_cross_program()")]
1510 #[inline]
1511 pub fn load_foreign<'a>(
1512 account: &'a $crate::hopper_runtime::AccountView,
1513 expected_owner: &$crate::hopper_runtime::Address,
1514 ) -> Result<
1515 $crate::hopper_core::account::VerifiedAccount<'a, Self>,
1516 $crate::hopper_runtime::error::ProgramError,
1517 > {
1518 Self::load_cross_program(account, expected_owner)
1519 }
1520
1521 /// Tier 2: Cross-program load (read-only).
1522 ///
1523 /// Validates: owner + layout_id + exact size.
1524 /// The layout_id is the ABI proof, so no discriminator or version check is needed.
1525 #[inline]
1526 pub fn load_cross_program<'a>(
1527 account: &'a $crate::hopper_runtime::AccountView,
1528 expected_owner: &$crate::hopper_runtime::Address,
1529 ) -> Result<
1530 $crate::hopper_core::account::VerifiedAccount<'a, Self>,
1531 $crate::hopper_runtime::error::ProgramError,
1532 > {
1533 $crate::hopper_core::check::check_owner(account, expected_owner)?;
1534 let data = account.try_borrow()?;
1535 let layout_id = $crate::hopper_core::account::read_layout_id(&*data)?;
1536 if layout_id != Self::LAYOUT_ID {
1537 return Err($crate::hopper_runtime::error::ProgramError::InvalidAccountData);
1538 }
1539 $crate::hopper_core::check::check_size(&*data, Self::LEN)?;
1540 $crate::hopper_core::account::VerifiedAccount::from_ref(data)
1541 }
1542
1543 /// Tier 2m: Foreign load with multiple possible owners.
1544 ///
1545 /// Returns `(VerifiedAccount, owner_index)` where `owner_index`
1546 /// indicates which expected owner matched.
1547 #[inline]
1548 pub fn load_foreign_multi<'a>(
1549 account: &'a $crate::hopper_runtime::AccountView,
1550 owners: &[&$crate::hopper_runtime::Address],
1551 ) -> Result<
1552 ($crate::hopper_core::account::VerifiedAccount<'a, Self>, usize),
1553 $crate::hopper_runtime::error::ProgramError,
1554 > {
1555 let owner_idx = $crate::hopper_core::check::check_owner_multi(account, owners)?;
1556 let data = account.try_borrow()?;
1557 let layout_id = $crate::hopper_core::account::read_layout_id(&*data)?;
1558 if layout_id != Self::LAYOUT_ID {
1559 return Err($crate::hopper_runtime::error::ProgramError::InvalidAccountData);
1560 }
1561 $crate::hopper_core::check::check_size(&*data, Self::LEN)?;
1562 let verified = $crate::hopper_core::account::VerifiedAccount::from_ref(data)?;
1563 Ok((verified, owner_idx))
1564 }
1565
1566 /// Load with a TrustProfile for configurable cross-program validation.
1567 ///
1568 /// Supports Strict, Compatible, and Observational trust levels.
1569 #[inline]
1570 pub fn load_with_profile<'a>(
1571 account: &'a $crate::hopper_runtime::AccountView,
1572 profile: &$crate::hopper_core::check::trust::TrustProfile<'a>,
1573 ) -> Result<
1574 $crate::hopper_core::account::VerifiedAccount<'a, Self>,
1575 $crate::hopper_runtime::error::ProgramError,
1576 > {
1577 let data = profile.load(account)?;
1578 $crate::hopper_core::account::VerifiedAccount::from_ref(data)
1579 }
1580
1581 /// Tier 5: Unverified overlay for indexers/tooling.
1582 #[inline]
1583 pub fn load_unverified(data: &[u8]) -> Option<(&Self, bool)> {
1584 if data.len() < Self::LEN {
1585 return None;
1586 }
1587 let validated = $crate::hopper_core::account::check_header(
1588 data,
1589 Self::DISC,
1590 Self::VERSION,
1591 &Self::LAYOUT_ID,
1592 )
1593 .is_ok();
1594 // SAFETY: Size checked above. T: Pod, alignment-1.
1595 let overlay = unsafe { &*(data.as_ptr() as *const Self) };
1596 Some((overlay, validated))
1597 }
1598 }
1599 };
1600}
1601
1602// ═════════════════════════════════════════════════════════════════════
1603// Section: Typed account-struct context generation
1604// ═════════════════════════════════════════════════════════════════════
1605
1606/// Generate a typed instruction context struct with validated account parsing.
1607///
1608/// Produces:
1609/// - The account struct itself
1610/// - A `Bumps` struct for PDA bump storage
1611/// - A `HopperAccounts` impl with `try_from_accounts`
1612/// - A static `ContextDescriptor` for schema/explain
1613///
1614/// # Account kinds
1615///
1616/// Each field specifies a `kind` wrapped in parentheses, with optional modifiers:
1617///
1618/// | Kind | Description | Writable | Signer |
1619/// |----------------------|---------------------------------|----------|--------|
1620/// | `(signer)` | Verified signer (SignerAccount) | no | yes |
1621/// | `(mut signer)` | Mutable + signer | yes | yes |
1622/// | `(account<T>)` | Layout-bound HopperAccount | no | no |
1623/// | `(mut account<T>)` | Mutable layout-bound account | yes | no |
1624/// | `(program)` | Verified executable (ProgramRef)| no | no |
1625/// | `(unchecked)` | No-validation passthrough | no | no |
1626/// | `(mut unchecked)` | Mutable unchecked passthrough | yes | no |
1627///
1628/// # Example
1629///
1630/// ```ignore
1631/// hopper_accounts! {
1632/// pub struct Deposit {
1633/// authority: (mut signer),
1634/// vault: (mut account<VaultState>),
1635/// system_program: (program),
1636/// }
1637/// }
1638/// ```
1639///
1640/// Then use with `hopper_entry`:
1641///
1642/// ```ignore
1643/// hopper_entry::<DepositIx, _>(program_id, accounts, data, |ctx, args| {
1644/// let vault = ctx.accounts.vault.write()?;
1645/// // ...
1646/// Ok(())
1647/// })
1648/// ```
1649#[macro_export]
1650macro_rules! hopper_accounts {
1651 // Main entry: parse struct with field list
1652 (
1653 $(#[$attr:meta])*
1654 pub struct $name:ident {
1655 $( $field:ident : ( $($kind:tt)+ ) ),+ $(,)?
1656 }
1657 ) => {
1658 // Wrap each kind in parens so it becomes a single tt group,
1659 // which eliminates the greedy-tt ambiguity in the inner macro.
1660 $crate::_hopper_accounts_struct!($name; $( $field: ($($kind)+) ; )+);
1661 };
1662}
1663
1664/// Internal: parse each field's kind and generate the struct + impls.
1665///
1666/// Each `$kind` is a single parenthesised token tree, e.g. `(mut signer)`.
1667#[doc(hidden)]
1668#[macro_export]
1669macro_rules! _hopper_accounts_struct {
1670 ($name:ident; $( $field:ident : $kind:tt ; )+) => {
1671
1672 // --- Context struct ---
1673 pub struct $name<'a> {
1674 $(
1675 pub $field: $crate::_hopper_field_type!($kind),
1676 )+
1677 }
1678
1679 // --- HopperAccounts impl ---
1680 impl<'a> $crate::hopper_core::accounts::HopperAccounts<'a> for $name<'a> {
1681 type Bumps = ();
1682
1683 const ACCOUNT_COUNT: usize = {
1684 // Count fields at compile time using the array-length trick.
1685 #[allow(unused)]
1686 const N: usize = [$( { let _ = stringify!($field); 0u8 }, )+].len();
1687 N
1688 };
1689
1690 fn try_from_accounts(
1691 program_id: &'a $crate::hopper_runtime::Address,
1692 accounts: &'a [$crate::hopper_runtime::AccountView],
1693 _instruction_data: &'a [u8],
1694 ) -> Result<(Self, Self::Bumps), $crate::hopper_runtime::error::ProgramError> {
1695 let mut _idx: usize = 0;
1696 $(
1697 if _idx >= accounts.len() {
1698 return Err($crate::hopper_runtime::error::ProgramError::NotEnoughAccountKeys);
1699 }
1700 let $field = $crate::_hopper_field_parse!(
1701 &accounts[_idx], program_id, $kind
1702 )?;
1703 _idx += 1;
1704 )+
1705 Ok((Self { $( $field, )+ }, ()))
1706 }
1707
1708 #[cfg(feature = "explain")]
1709 fn context_schema() -> Option<
1710 &'static $crate::hopper_core::accounts::explain::ContextSchema
1711 > {
1712 static FIELDS: &[$crate::hopper_core::accounts::explain::AccountFieldSchema] = &[
1713 $(
1714 $crate::hopper_core::accounts::explain::AccountFieldSchema {
1715 name: stringify!($field),
1716 kind: $crate::_hopper_field_kind_name!($kind),
1717 mutable: $crate::_hopper_field_is_mut!($kind),
1718 signer: $crate::_hopper_field_is_signer!($kind),
1719 layout: $crate::_hopper_field_layout_name!($kind),
1720 policy: None,
1721 seeds: &[],
1722 optional: false,
1723 },
1724 )+
1725 ];
1726 static SCHEMA: $crate::hopper_core::accounts::explain::ContextSchema =
1727 $crate::hopper_core::accounts::explain::ContextSchema {
1728 name: stringify!($name),
1729 fields: FIELDS,
1730 policy_names: &[],
1731 receipts_expected: false,
1732 mutation_classes: &[],
1733 };
1734 Some(&SCHEMA)
1735 }
1736 }
1737 };
1738}
1739
1740// --- Field type resolution ---
1741
1742#[doc(hidden)]
1743#[macro_export]
1744macro_rules! _hopper_field_type {
1745 ((mut signer)) => { $crate::hopper_core::accounts::SignerAccount<'a> };
1746 ((signer)) => { $crate::hopper_core::accounts::SignerAccount<'a> };
1747 ((mut account < $layout:ty >)) => { $crate::hopper_core::accounts::HopperAccount<'a, $layout> };
1748 ((account < $layout:ty >)) => { $crate::hopper_core::accounts::HopperAccount<'a, $layout> };
1749 ((program)) => { $crate::hopper_core::accounts::ProgramRef<'a> };
1750 ((unchecked)) => { $crate::hopper_core::accounts::UncheckedAccount<'a> };
1751 ((mut unchecked)) => { $crate::hopper_core::accounts::UncheckedAccount<'a> };
1752}
1753
1754// --- Field parsing at runtime ---
1755
1756#[doc(hidden)]
1757#[macro_export]
1758macro_rules! _hopper_field_parse {
1759 ($account:expr, $program_id:expr, (mut signer)) => {{
1760 $crate::hopper_core::check::check_writable($account)?;
1761 $crate::hopper_core::accounts::SignerAccount::from_account($account)
1762 }};
1763 ($account:expr, $program_id:expr, (signer)) => {
1764 $crate::hopper_core::accounts::SignerAccount::from_account($account)
1765 };
1766 ($account:expr, $program_id:expr, (mut account < $layout:ty >)) => {
1767 $crate::hopper_core::accounts::HopperAccount::<$layout>::from_account_mut(
1768 $account,
1769 $program_id,
1770 )
1771 };
1772 ($account:expr, $program_id:expr, (account < $layout:ty >)) => {
1773 $crate::hopper_core::accounts::HopperAccount::<$layout>::from_account($account, $program_id)
1774 };
1775 ($account:expr, $program_id:expr, (program)) => {
1776 $crate::hopper_core::accounts::ProgramRef::from_account($account)
1777 };
1778 ($account:expr, $program_id:expr, (unchecked)) => {
1779 Ok::<_, $crate::hopper_runtime::error::ProgramError>(
1780 $crate::hopper_core::accounts::UncheckedAccount::new($account),
1781 )
1782 };
1783 ($account:expr, $program_id:expr, (mut unchecked)) => {{
1784 $crate::hopper_core::check::check_writable($account)?;
1785 Ok::<_, $crate::hopper_runtime::error::ProgramError>(
1786 $crate::hopper_core::accounts::UncheckedAccount::new($account),
1787 )
1788 }};
1789}
1790
1791// --- Static metadata helpers ---
1792
1793#[doc(hidden)]
1794#[macro_export]
1795macro_rules! _hopper_field_kind_name {
1796 ((mut signer)) => {
1797 "Signer"
1798 };
1799 ((signer)) => {
1800 "Signer"
1801 };
1802 ((mut account < $layout:ty >)) => {
1803 "HopperAccount"
1804 };
1805 ((account < $layout:ty >)) => {
1806 "HopperAccount"
1807 };
1808 ((program)) => {
1809 "ProgramRef"
1810 };
1811 ((unchecked)) => {
1812 "Unchecked"
1813 };
1814 ((mut unchecked)) => {
1815 "Unchecked"
1816 };
1817}
1818
1819#[doc(hidden)]
1820#[macro_export]
1821macro_rules! _hopper_field_is_mut {
1822 ((mut signer)) => {
1823 true
1824 };
1825 ((signer)) => {
1826 false
1827 };
1828 ((mut account < $layout:ty >)) => {
1829 true
1830 };
1831 ((account < $layout:ty >)) => {
1832 false
1833 };
1834 ((program)) => {
1835 false
1836 };
1837 ((unchecked)) => {
1838 false
1839 };
1840 ((mut unchecked)) => {
1841 true
1842 };
1843}
1844
1845#[doc(hidden)]
1846#[macro_export]
1847macro_rules! _hopper_field_is_signer {
1848 ((mut signer)) => {
1849 true
1850 };
1851 ((signer)) => {
1852 true
1853 };
1854 ((mut account < $layout:ty >)) => {
1855 false
1856 };
1857 ((account < $layout:ty >)) => {
1858 false
1859 };
1860 ((program)) => {
1861 false
1862 };
1863 ((unchecked)) => {
1864 false
1865 };
1866 ((mut unchecked)) => {
1867 false
1868 };
1869}
1870
1871#[doc(hidden)]
1872#[macro_export]
1873macro_rules! _hopper_field_layout_name {
1874 ((mut signer)) => {
1875 None
1876 };
1877 ((signer)) => {
1878 None
1879 };
1880 ((mut account < $layout:ty >)) => {
1881 Some(stringify!($layout))
1882 };
1883 ((account < $layout:ty >)) => {
1884 Some(stringify!($layout))
1885 };
1886 ((program)) => {
1887 None
1888 };
1889 ((unchecked)) => {
1890 None
1891 };
1892 ((mut unchecked)) => {
1893 None
1894 };
1895}