jiminy_core/interface.rs
1//! Cross-program ABI interface for read-only foreign account access.
2//!
3//! The `jiminy_interface!` macro generates a lightweight,
4//! read-only struct that can decode accounts owned by another program.
5//! It produces the same `LAYOUT_ID` as a matching `zero_copy_layout!`
6//! declaration, enabling cross-program layout verification without
7//! sharing crate dependencies.
8//!
9//! ## Usage
10//!
11//! Program B wants to read Program A's `Vault` account:
12//!
13//! ```rust,ignore
14//! use jiminy_core::jiminy_interface;
15//! use jiminy_core::account::{AccountHeader, Pod, FixedLayout};
16//! use jiminy_core::abi::LeU64;
17//! use hopper_runtime::Address;
18//!
19//! const PROGRAM_A: Address = [0u8; 32]; // Program A's address
20//!
21//! jiminy_interface! {
22//! /// Read-only view of Program A's Vault account.
23//! pub struct Vault for PROGRAM_A {
24//! header: AccountHeader = 16,
25//! balance: LeU64 = 8,
26//! authority: Address = 32,
27//! }
28//! }
29//!
30//! // In your instruction handler:
31//! fn process(accounts: &[AccountView]) -> ProgramResult {
32//! let verified = Vault::load_foreign(&accounts[0])?;
33//! let vault = verified.get();
34//! // read vault.balance, vault.authority, etc.
35//! Ok(())
36//! }
37//! ```
38//!
39//! ## What gets generated
40//!
41//! - `#[repr(C)]` struct with typed fields
42//! - `LAYOUT_ID` matching the original `zero_copy_layout!` definition
43//! - `LEN` constant
44//! - `overlay` / `read` (immutable only, no mutable access)
45//! - `load_foreign` with Tier 2 owner + layout_id validation
46//! - Const field offsets and `split_fields` (immutable only)
47//!
48//! ## Version
49//!
50//! By default, `version = 1` is used in the `LAYOUT_ID` hash. If the
51//! foreign program uses a different version, specify it explicitly:
52//!
53//! ```rust,ignore
54//! jiminy_interface! {
55//! pub struct PoolV2 for PROGRAM_A, version = 2 { ... }
56//! }
57//! ```
58//!
59//! ## Design
60//!
61//! Interface types are intentionally restricted:
62//! - **No `load` (Tier 1)**: you don't own this account
63//! - **No `overlay_mut`**: foreign accounts are read-only
64//! - **No `split_fields_mut`**: same reason
65//! - **No `load_checked`**: discriminator/version are owner concerns
66
67/// Declare a read-only interface for a foreign program's account layout.
68///
69/// Generates a `#[repr(C)]` struct with the same `LAYOUT_ID` as the
70/// foreign program's `zero_copy_layout!` definition, plus a
71/// `load_foreign` method that validates owner + layout_id.
72///
73/// The struct name must match the original account name for the
74/// `LAYOUT_ID` hash to agree. If you want a local alias, use
75/// `type VaultView = Vault;` after the macro invocation.
76///
77/// ## Version
78///
79/// By default, the interface assumes the foreign program uses
80/// `version = 1`. If the foreign layout uses a different version,
81/// specify it explicitly so the `LAYOUT_ID` hash matches:
82///
83/// ```rust,ignore
84/// jiminy_interface! {
85/// pub struct PoolV2 for PROGRAM_A, version = 2 {
86/// header: AccountHeader = 16,
87/// authority: Address = 32,
88/// reserve: LeU64 = 8,
89/// fee_bps: LeU16 = 2,
90/// }
91/// }
92/// ```
93#[macro_export]
94macro_rules! jiminy_interface {
95 // ── Public arm: no version (defaults to 1) ───────────────────
96 (
97 $(#[$meta:meta])*
98 $vis:vis struct $name:ident for $owner:path {
99 $( $(#[$fmeta:meta])* $field:ident : $fty:ident = $fsize:expr ),+ $(,)?
100 }
101 ) => {
102 $crate::jiminy_interface! {
103 @impl version = 1,
104 $(#[$meta])*
105 $vis struct $name for $owner {
106 $( $(#[$fmeta])* $field : $fty = $fsize ),+
107 }
108 }
109 };
110
111 // ── Public arm: explicit version ─────────────────────────────
112 (
113 $(#[$meta:meta])*
114 $vis:vis struct $name:ident for $owner:path, version = $ver:literal {
115 $( $(#[$fmeta:meta])* $field:ident : $fty:ident = $fsize:expr ),+ $(,)?
116 }
117 ) => {
118 $crate::jiminy_interface! {
119 @impl version = $ver,
120 $(#[$meta])*
121 $vis struct $name for $owner {
122 $( $(#[$fmeta])* $field : $fty = $fsize ),+
123 }
124 }
125 };
126
127 // ── Internal implementation arm ──────────────────────────────
128 (
129 @impl version = $ver:literal,
130 $(#[$meta:meta])*
131 $vis:vis struct $name:ident for $owner:path {
132 $( $(#[$fmeta:meta])* $field:ident : $fty:ident = $fsize:expr ),+ $(,)?
133 }
134 ) => {
135 $(#[$meta])*
136 #[repr(C)]
137 #[derive(Clone, Copy)]
138 $vis struct $name {
139 $( $(#[$fmeta])* pub $field: $fty ),+
140 }
141
142 // SAFETY: repr(C) + Copy + all fields are Pod.
143 unsafe impl $crate::account::Pod for $name {}
144
145 impl $crate::account::FixedLayout for $name {
146 const SIZE: usize = 0 $( + $fsize )+;
147 }
148
149 // Compile-time assertion: actual size must match declared sum.
150 const _: () = assert!(
151 core::mem::size_of::<$name>() == 0 $( + $fsize )+,
152 "size_of does not match declared LEN; check field sizes"
153 );
154
155 // Compile-time assertion: alignment must not exceed 8 bytes.
156 const _: () = assert!(
157 core::mem::align_of::<$name>() <= 8,
158 "layout alignment exceeds 8 bytes; use Le* wrappers for u128 fields"
159 );
160
161 impl $name {
162 /// Total byte size of this account layout.
163 pub const LEN: usize = 0 $( + $fsize )+;
164
165 /// Deterministic ABI fingerprint (first 8 bytes of SHA-256).
166 ///
167 /// The version used in the hash input matches the foreign
168 /// program's `zero_copy_layout!` version. When no version is
169 /// specified in the macro invocation, version 1 is assumed.
170 pub const LAYOUT_ID: [u8; 8] = {
171 const INPUT: &str = concat!(
172 "jiminy:v1:",
173 stringify!($name), ":",
174 stringify!($ver), ":",
175 $( stringify!($field), ":", $crate::__canonical_type!($fty), ":", stringify!($fsize), ",", )+
176 );
177 const HASH: [u8; 32] = $crate::__sha256_const(INPUT.as_bytes());
178 [HASH[0], HASH[1], HASH[2], HASH[3], HASH[4], HASH[5], HASH[6], HASH[7]]
179 };
180
181 /// Expected owner program for this interface.
182 pub const OWNER: &'static $crate::Address = &$owner;
183
184 /// Overlay an immutable reference onto borrowed account data.
185 #[inline(always)]
186 pub fn overlay(data: &[u8]) -> Result<&Self, $crate::ProgramError> {
187 $crate::account::pod_from_bytes::<Self>(data)
188 }
189
190 /// Read a copy of this struct from a byte slice (alignment-safe).
191 #[inline(always)]
192 pub fn read(data: &[u8]) -> Result<Self, $crate::ProgramError> {
193 $crate::account::pod_read::<Self>(data)
194 }
195
196 /// **Tier 2: Cross-program read.** Validate owner + layout_id
197 /// + exact size, then borrow.
198 ///
199 /// The owner is checked against the program address passed to
200 /// `jiminy_interface!` via the `for` clause.
201 ///
202 /// Returns a `VerifiedAccount` whose `get()` is infallible.
203 #[inline(always)]
204 pub fn load_foreign<'a>(
205 account: &'a $crate::AccountView,
206 ) -> Result<$crate::account::VerifiedAccount<'a, Self>, $crate::ProgramError> {
207 let data = $crate::account::view::validate_foreign(
208 account, &$owner, &Self::LAYOUT_ID, Self::LEN,
209 )?;
210 $crate::account::VerifiedAccount::new(data)
211 }
212
213 // ── Const field offsets ──────────────────────────────────────
214
215 $crate::__gen_offsets!( $( $field = $fsize ),+ );
216
217 // ── Immutable borrow-splitting ───────────────────────────────
218
219 /// Split borrowed data into per-field `FieldRef` slices.
220 #[inline]
221 #[allow(unused_variables)]
222 pub fn split_fields(data: &[u8]) -> Result<( $( $crate::__field_ref_type!($field), )+ ), $crate::ProgramError> {
223 if data.len() < Self::LEN {
224 return Err($crate::ProgramError::AccountDataTooSmall);
225 }
226 let mut _pos = 0usize;
227 Ok(( $({
228 let start = _pos;
229 _pos += $fsize;
230 $crate::abi::FieldRef::new(&data[start..start + $fsize])
231 }, )+ ))
232 }
233 }
234 };
235}
236
237// ═══════════════════════════════════════════════════════════════════════════════
238// segmented_interface!: read-only cross-program view for segmented accounts
239// ═══════════════════════════════════════════════════════════════════════════════
240
241/// Declare a read-only interface for a foreign program's segmented account.
242///
243/// Extends [`jiminy_interface!`] with segment declarations, generating
244/// the same `SEGMENTED_LAYOUT_ID` as the foreign program's
245/// `segmented_layout!` definition. This enables cross-program reads of
246/// variable-length accounts (order books, staking pools, etc.) without
247/// crate dependencies.
248///
249/// ## Usage
250///
251/// Program B wants to read Program A's `OrderBook` segmented account:
252///
253/// ```rust,ignore
254/// use jiminy_core::segmented_interface;
255/// use jiminy_core::account::{AccountHeader, Pod, FixedLayout};
256/// use jiminy_core::abi::LeU64;
257/// use hopper_runtime::Address;
258///
259/// const DEX_PROGRAM: Address = [0u8; 32];
260///
261/// // Order element type (must match Program A's definition)
262/// #[repr(C)]
263/// #[derive(Clone, Copy)]
264/// struct Order {
265/// price: LeU64,
266/// size: LeU64,
267/// }
268/// unsafe impl Pod for Order {}
269/// impl FixedLayout for Order { const SIZE: usize = 16; }
270///
271/// segmented_interface! {
272/// pub struct OrderBook for DEX_PROGRAM {
273/// header: AccountHeader = 16,
274/// market: Address = 32,
275/// } segments {
276/// bids: Order = 16,
277/// asks: Order = 16,
278/// }
279/// }
280///
281/// // In your instruction handler:
282/// fn process(accounts: &[AccountView]) -> ProgramResult {
283/// let data = OrderBook::load_foreign_segmented(&accounts[0])?;
284/// let table = OrderBook::segment_table(&data)?;
285/// let bids = SegmentSlice::<Order>::from_descriptor(&data, &table.descriptor(0)?)?;
286/// // read bids...
287/// Ok(())
288/// }
289/// ```
290///
291/// ## What gets generated
292///
293/// Everything from `jiminy_interface!` plus:
294///
295/// - `SEGMENTED_LAYOUT_ID` matching the foreign `segmented_layout!`
296/// - `SEGMENT_COUNT`, `TABLE_OFFSET`, `DATA_START_OFFSET`, `MIN_ACCOUNT_SIZE`
297/// - `segment_table(data)`: read-only segment table access
298/// - `segment::<T>(data, index)`: typed read-only segment slice
299/// - `validate_segments(data)`: full segment validation
300/// - `load_foreign_segmented(account)`: Tier 2 validation with min-size
301///
302/// No mutable access is generated (consistent with interface philosophy).
303#[macro_export]
304macro_rules! segmented_interface {
305 // ── Public arm: no version (defaults to 1) ───────────────────
306 (
307 $(#[$meta:meta])*
308 $vis:vis struct $name:ident for $owner:path {
309 $( $(#[$fmeta:meta])* $field:ident : $fty:ident = $fsize:expr ),+ $(,)?
310 } segments {
311 $( $seg_name:ident : $seg_ty:ident = $seg_elem_size:expr ),+ $(,)?
312 }
313 ) => {
314 $crate::segmented_interface! {
315 @impl version = 1,
316 $(#[$meta])*
317 $vis struct $name for $owner {
318 $( $(#[$fmeta])* $field : $fty = $fsize ),+
319 } segments {
320 $( $seg_name : $seg_ty = $seg_elem_size ),+
321 }
322 }
323 };
324
325 // ── Public arm: explicit version ─────────────────────────────
326 (
327 $(#[$meta:meta])*
328 $vis:vis struct $name:ident for $owner:path, version = $ver:literal {
329 $( $(#[$fmeta:meta])* $field:ident : $fty:ident = $fsize:expr ),+ $(,)?
330 } segments {
331 $( $seg_name:ident : $seg_ty:ident = $seg_elem_size:expr ),+ $(,)?
332 }
333 ) => {
334 $crate::segmented_interface! {
335 @impl version = $ver,
336 $(#[$meta])*
337 $vis struct $name for $owner {
338 $( $(#[$fmeta])* $field : $fty = $fsize ),+
339 } segments {
340 $( $seg_name : $seg_ty = $seg_elem_size ),+
341 }
342 }
343 };
344
345 // ── Internal implementation arm ──────────────────────────────
346 (
347 @impl version = $ver:literal,
348 $(#[$meta:meta])*
349 $vis:vis struct $name:ident for $owner:path {
350 $( $(#[$fmeta:meta])* $field:ident : $fty:ident = $fsize:expr ),+ $(,)?
351 } segments {
352 $( $seg_name:ident : $seg_ty:ident = $seg_elem_size:expr ),+ $(,)?
353 }
354 ) => {
355 // Generate the fixed prefix struct via jiminy_interface!
356 $crate::jiminy_interface! {
357 @impl version = $ver,
358 $(#[$meta])*
359 $vis struct $name for $owner {
360 $( $(#[$fmeta])* $field : $fty = $fsize ),+
361 }
362 }
363
364 impl $name {
365 /// Number of dynamic segments in this layout.
366 pub const SEGMENT_COUNT: usize = $crate::__count_segments!($($seg_name)+);
367
368 /// Byte size of the fixed prefix (before the segment table).
369 pub const FIXED_LEN: usize = Self::LEN;
370
371 /// Byte offset where the segment table begins.
372 pub const TABLE_OFFSET: usize = Self::LEN;
373
374 /// Byte offset where segment data starts (after fixed + table).
375 pub const DATA_START_OFFSET: usize =
376 Self::LEN + Self::SEGMENT_COUNT * $crate::account::segment::SEGMENT_DESC_SIZE;
377
378 /// Minimum account size: fixed prefix + segment table (no data).
379 pub const MIN_ACCOUNT_SIZE: usize = Self::DATA_START_OFFSET;
380
381 /// Deterministic ABI fingerprint including segment declarations.
382 ///
383 /// Produces the same hash as the foreign `segmented_layout!`.
384 pub const SEGMENTED_LAYOUT_ID: [u8; 8] = {
385 const INPUT: &str = concat!(
386 "jiminy:v1:",
387 stringify!($name), ":",
388 stringify!($ver), ":",
389 $( stringify!($field), ":", $crate::__canonical_type!($fty), ":", stringify!($fsize), ",", )+
390 $( "seg:", stringify!($seg_name), ":", stringify!($seg_ty), ":", stringify!($seg_elem_size), ",", )+
391 );
392 const HASH: [u8; 32] = $crate::__sha256_const(INPUT.as_bytes());
393 [HASH[0], HASH[1], HASH[2], HASH[3], HASH[4], HASH[5], HASH[6], HASH[7]]
394 };
395
396 /// Expected element sizes for each segment, in declaration order.
397 #[inline(always)]
398 pub const fn segment_sizes() -> &'static [u16] {
399 &[ $( $seg_elem_size as u16, )+ ]
400 }
401
402 /// **Tier 2: Cross-program segmented read.**
403 ///
404 /// Validates owner + `SEGMENTED_LAYOUT_ID` + minimum size,
405 /// then borrows account data. Returns the raw data reference
406 /// for segment table and element access.
407 ///
408 /// Unlike `load_foreign` on fixed layouts (which returns a
409 /// `VerifiedAccount<T>`), this returns the raw borrowed data
410 /// because the account size is variable. Use
411 /// [`segment_table`](Self::segment_table) and
412 /// [`segment`](Self::segment) for typed access.
413 #[inline(always)]
414 pub fn load_foreign_segmented<'a>(
415 account: &'a $crate::AccountView,
416 ) -> Result<$crate::hopper_runtime::Ref<'a, [u8]>, $crate::ProgramError> {
417 $crate::account::view::validate_foreign_segmented(
418 account,
419 &$owner,
420 &Self::SEGMENTED_LAYOUT_ID,
421 Self::MIN_ACCOUNT_SIZE,
422 )
423 }
424
425 /// Read the segment table from account data (read-only).
426 #[inline(always)]
427 pub fn segment_table(data: &[u8]) -> Result<$crate::account::segment::SegmentTable<'_>, $crate::ProgramError> {
428 if data.len() < Self::DATA_START_OFFSET {
429 return Err($crate::ProgramError::AccountDataTooSmall);
430 }
431 $crate::account::segment::SegmentTable::from_bytes(
432 &data[Self::TABLE_OFFSET..],
433 Self::SEGMENT_COUNT,
434 )
435 }
436
437 /// Validate the segment table against the account data.
438 ///
439 /// Checks element sizes, bounds, ordering, and no overlaps.
440 #[inline]
441 pub fn validate_segments(data: &[u8]) -> Result<(), $crate::ProgramError> {
442 let table = Self::segment_table(data)?;
443 table.validate(data.len(), Self::segment_sizes(), Self::DATA_START_OFFSET)
444 }
445
446 /// Get an immutable typed view over a segment by index.
447 #[inline(always)]
448 pub fn segment<T: $crate::account::Pod + $crate::account::FixedLayout>(
449 data: &[u8],
450 index: usize,
451 ) -> Result<$crate::account::segment::SegmentSlice<'_, T>, $crate::ProgramError> {
452 let desc = {
453 let table = Self::segment_table(data)?;
454 table.descriptor(index)?
455 };
456 $crate::account::segment::SegmentSlice::from_descriptor(data, &desc)
457 }
458
459 // ── Segment index constants ──────────────────────────────
460
461 $crate::__gen_segment_indices!($($seg_name),+);
462 }
463 };
464}