Skip to main content

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}