#![no_std]
#[macro_export]
macro_rules! hopper_layout {
(
$(#[$attr:meta])*
pub struct $name:ident, disc = $disc:literal, version = $ver:literal
{
$(
$(#[$field_attr:meta])*
$field:ident : $fty:ty = $fsize:literal
),+ $(,)?
}
) => {
$(#[$attr])*
#[derive(Clone, Copy)]
#[repr(C)]
pub struct $name {
pub header: $crate::hopper_core::account::AccountHeader,
$(
$(#[$field_attr])*
pub $field: $fty,
)+
}
const _: () = {
let expected = $crate::hopper_core::account::HEADER_LEN $( + $fsize )+;
assert!(
core::mem::size_of::<$name>() == expected,
"Layout size mismatch: struct size != declared field sizes + header"
);
assert!(
core::mem::align_of::<$name>() == 1,
"Layout alignment must be 1 for zero-copy safety"
);
};
#[cfg(feature = "hopper-native-backend")]
unsafe impl $crate::hopper_runtime::__hopper_native::bytemuck::Zeroable for $name {}
#[cfg(feature = "hopper-native-backend")]
unsafe impl $crate::hopper_runtime::__hopper_native::bytemuck::Pod for $name {}
unsafe impl $crate::hopper_core::account::Pod for $name {}
unsafe impl $crate::hopper_runtime::__sealed::HopperZeroCopySealed for $name {}
impl $crate::hopper_core::account::FixedLayout for $name {
const SIZE: usize = $crate::hopper_core::account::HEADER_LEN $( + $fsize )+;
}
impl $crate::hopper_core::field_map::FieldMap for $name {
const FIELDS: &'static [$crate::hopper_core::field_map::FieldInfo] = {
const FIELD_COUNT: usize = 0 $( + { let _ = stringify!($field); 1 } )+;
const NAMES: [&str; FIELD_COUNT] = [ $( stringify!($field) ),+ ];
const SIZES: [usize; FIELD_COUNT] = [ $( $fsize ),+ ];
const FIELDS: [$crate::hopper_core::field_map::FieldInfo; FIELD_COUNT] = {
let mut result = [$crate::hopper_core::field_map::FieldInfo::new("", 0, 0); FIELD_COUNT];
let mut offset = $crate::hopper_core::account::HEADER_LEN;
let mut index = 0;
while index < FIELD_COUNT {
result[index] = $crate::hopper_core::field_map::FieldInfo::new(
NAMES[index],
offset,
SIZES[index],
);
offset += SIZES[index];
index += 1;
}
result
};
&FIELDS
};
}
impl $crate::hopper_runtime::LayoutContract for $name {
const DISC: u8 = $disc;
const VERSION: u8 = $ver;
const LAYOUT_ID: [u8; 8] = $name::LAYOUT_ID;
const SIZE: usize = $name::LEN;
const TYPE_OFFSET: usize = 0;
}
impl $crate::hopper_schema::SchemaExport for $name {
fn layout_manifest() -> $crate::hopper_schema::LayoutManifest {
const FIELD_COUNT: usize = 0 $( + { let _ = stringify!($field); 1 } )+;
const SIZES: [u16; FIELD_COUNT] = [ $( $fsize ),+ ];
const NAMES: [&str; FIELD_COUNT] = [ $( stringify!($field) ),+ ];
const TYPES: [&str; FIELD_COUNT] = [ $( stringify!($fty) ),+ ];
const FIELDS: [$crate::hopper_schema::FieldDescriptor; FIELD_COUNT] = {
let mut result = [$crate::hopper_schema::FieldDescriptor {
name: "", canonical_type: "", size: 0, offset: 0,
intent: $crate::hopper_schema::FieldIntent::Custom,
}; FIELD_COUNT];
let mut offset = $crate::hopper_core::account::HEADER_LEN as u16;
let mut index = 0;
while index < FIELD_COUNT {
result[index] = $crate::hopper_schema::FieldDescriptor {
name: NAMES[index],
canonical_type: TYPES[index],
size: SIZES[index],
offset,
intent: $crate::hopper_schema::FieldIntent::Custom,
};
offset += SIZES[index];
index += 1;
}
result
};
$crate::hopper_schema::LayoutManifest {
name: stringify!($name),
version: <$name>::VERSION,
disc: <$name>::DISC,
layout_id: <$name>::LAYOUT_ID,
total_size: <$name>::LEN,
field_count: FIELD_COUNT,
fields: &FIELDS,
}
}
}
impl $name {
pub const LEN: usize = $crate::hopper_core::account::HEADER_LEN $( + $fsize )+;
pub const DISC: u8 = $disc;
pub const VERSION: u8 = $ver;
pub const LAYOUT_ID: [u8; 8] = {
const INPUT: &str = concat!(
"hopper:v1:",
stringify!($name), ":",
stringify!($ver), ":",
$( stringify!($field), ":", stringify!($fty), ":", stringify!($fsize), ",", )+
);
const HASH: [u8; 32] = $crate::hopper_core::__sha256_const(INPUT.as_bytes());
[
HASH[0], HASH[1], HASH[2], HASH[3],
HASH[4], HASH[5], HASH[6], HASH[7],
]
};
#[inline(always)]
pub fn overlay(data: &[u8]) -> Result<&Self, $crate::hopper_runtime::error::ProgramError> {
$crate::hopper_core::account::pod_from_bytes::<Self>(data)
}
#[inline(always)]
pub fn overlay_mut(data: &mut [u8]) -> Result<&mut Self, $crate::hopper_runtime::error::ProgramError> {
$crate::hopper_core::account::pod_from_bytes_mut::<Self>(data)
}
#[inline]
pub fn load<'a>(
account: &'a $crate::hopper_runtime::AccountView,
program_id: &$crate::hopper_runtime::Address,
) -> Result<
$crate::hopper_core::account::VerifiedAccount<'a, Self>,
$crate::hopper_runtime::error::ProgramError,
> {
$crate::hopper_core::check::check_owner(account, program_id)?;
let data = account.try_borrow()?;
$crate::hopper_core::account::check_header(
&*data,
Self::DISC,
Self::VERSION,
&Self::LAYOUT_ID,
)?;
$crate::hopper_core::check::check_size(&*data, Self::LEN)?;
$crate::hopper_core::account::VerifiedAccount::from_ref(data)
}
#[inline]
pub fn load_mut<'a>(
account: &'a $crate::hopper_runtime::AccountView,
program_id: &$crate::hopper_runtime::Address,
) -> Result<
$crate::hopper_core::account::VerifiedAccountMut<'a, Self>,
$crate::hopper_runtime::error::ProgramError,
> {
$crate::hopper_core::check::check_owner(account, program_id)?;
$crate::hopper_core::check::check_writable(account)?;
let data = account.try_borrow_mut()?;
$crate::hopper_core::account::check_header(
&*data,
Self::DISC,
Self::VERSION,
&Self::LAYOUT_ID,
)?;
$crate::hopper_core::check::check_size(&*data, Self::LEN)?;
$crate::hopper_core::account::VerifiedAccountMut::from_ref_mut(data)
}
#[deprecated(since = "0.2.0", note = "renamed to load_cross_program()")]
#[inline]
pub fn load_foreign<'a>(
account: &'a $crate::hopper_runtime::AccountView,
expected_owner: &$crate::hopper_runtime::Address,
) -> Result<
$crate::hopper_core::account::VerifiedAccount<'a, Self>,
$crate::hopper_runtime::error::ProgramError,
> {
$crate::hopper_core::check::check_owner(account, expected_owner)?;
let data = account.try_borrow()?;
let layout_id = $crate::hopper_core::account::read_layout_id(&*data)?;
if layout_id != Self::LAYOUT_ID {
return Err($crate::hopper_runtime::error::ProgramError::InvalidAccountData);
}
$crate::hopper_core::check::check_size(&*data, Self::LEN)?;
$crate::hopper_core::account::VerifiedAccount::from_ref(data)
}
#[inline]
pub fn load_cross_program<'a>(
account: &'a $crate::hopper_runtime::AccountView,
expected_owner: &$crate::hopper_runtime::Address,
) -> Result<
$crate::hopper_core::account::VerifiedAccount<'a, Self>,
$crate::hopper_runtime::error::ProgramError,
> {
$crate::hopper_core::check::check_owner(account, expected_owner)?;
let data = account.try_borrow()?;
let layout_id = $crate::hopper_core::account::read_layout_id(&*data)?;
if layout_id != Self::LAYOUT_ID {
return Err($crate::hopper_runtime::error::ProgramError::InvalidAccountData);
}
$crate::hopper_core::check::check_size(&*data, Self::LEN)?;
$crate::hopper_core::account::VerifiedAccount::from_ref(data)
}
#[inline]
pub fn load_compatible<'a>(
account: &'a $crate::hopper_runtime::AccountView,
program_id: &$crate::hopper_runtime::Address,
min_version: u8,
) -> Result<
$crate::hopper_core::account::VerifiedAccount<'a, Self>,
$crate::hopper_runtime::error::ProgramError,
> {
$crate::hopper_core::check::check_owner(account, program_id)?;
let data = account.try_borrow()?;
if data.len() < $crate::hopper_core::account::HEADER_LEN {
return Err($crate::hopper_runtime::error::ProgramError::AccountDataTooSmall);
}
if data[0] != Self::DISC {
return Err($crate::hopper_runtime::error::ProgramError::InvalidAccountData);
}
if data[1] < min_version {
return Err($crate::hopper_runtime::error::ProgramError::InvalidAccountData);
}
if data.len() < Self::LEN {
return Err($crate::hopper_runtime::error::ProgramError::AccountDataTooSmall);
}
$crate::hopper_core::account::VerifiedAccount::from_ref(data)
}
#[inline]
pub fn load_compatible_mut<'a>(
account: &'a $crate::hopper_runtime::AccountView,
program_id: &$crate::hopper_runtime::Address,
min_version: u8,
) -> Result<
$crate::hopper_core::account::VerifiedAccountMut<'a, Self>,
$crate::hopper_runtime::error::ProgramError,
> {
$crate::hopper_core::check::check_owner(account, program_id)?;
$crate::hopper_core::check::check_writable(account)?;
let data = account.try_borrow_mut()?;
if data.len() < $crate::hopper_core::account::HEADER_LEN {
return Err($crate::hopper_runtime::error::ProgramError::AccountDataTooSmall);
}
if data[0] != Self::DISC {
return Err($crate::hopper_runtime::error::ProgramError::InvalidAccountData);
}
if data[1] < min_version {
return Err($crate::hopper_runtime::error::ProgramError::InvalidAccountData);
}
if data.len() < Self::LEN {
return Err($crate::hopper_runtime::error::ProgramError::AccountDataTooSmall);
}
$crate::hopper_core::account::VerifiedAccountMut::from_ref_mut(data)
}
#[deprecated(since = "0.2.0", note = "use load() for safe access")]
#[inline(always)]
pub unsafe fn load_unchecked(data: &[u8]) -> &Self {
&*(data.as_ptr() as *const Self)
}
#[inline(always)]
pub fn write_init_header(data: &mut [u8]) -> Result<(), $crate::hopper_runtime::error::ProgramError> {
$crate::hopper_core::account::write_header(
data,
Self::DISC,
Self::VERSION,
&Self::LAYOUT_ID,
)
}
pub const BUMP_OFFSET: usize = {
let mut offset = $crate::hopper_core::account::HEADER_LEN;
let mut found = usize::MAX;
$(
if $crate::hopper_core::__str_eq(stringify!($field), "bump") {
found = offset;
}
offset += $fsize;
)+
let _ = offset;
found
};
#[inline(always)]
pub const fn has_bump_offset() -> bool {
Self::BUMP_OFFSET != usize::MAX
}
#[inline]
pub fn verify_pda_cached(
account: &$crate::hopper_runtime::AccountView,
seeds: &[&[u8]],
program_id: &$crate::hopper_runtime::Address,
) -> Result<(), $crate::hopper_runtime::error::ProgramError> {
if Self::BUMP_OFFSET == usize::MAX {
return Err($crate::hopper_runtime::error::ProgramError::InvalidArgument);
}
$crate::hopper_runtime::pda::verify_pda_from_stored_bump(
account, seeds, Self::BUMP_OFFSET, program_id,
)
}
#[inline]
pub fn load_unverified(data: &[u8]) -> Option<(&Self, bool)> {
if data.len() < Self::LEN {
return None;
}
let validated = $crate::hopper_core::account::check_header(
data,
Self::DISC,
Self::VERSION,
&Self::LAYOUT_ID,
)
.is_ok();
let overlay = unsafe { &*(data.as_ptr() as *const Self) };
Some((overlay, validated))
}
#[inline]
pub fn load_foreign_multi<'a>(
account: &'a $crate::hopper_runtime::AccountView,
owners: &[&$crate::hopper_runtime::Address],
) -> Result<
($crate::hopper_core::account::VerifiedAccount<'a, Self>, usize),
$crate::hopper_runtime::error::ProgramError,
> {
let owner_idx = $crate::hopper_core::check::check_owner_multi(account, owners)?;
let data = account.try_borrow()?;
let layout_id = $crate::hopper_core::account::read_layout_id(&*data)?;
if layout_id != Self::LAYOUT_ID {
return Err($crate::hopper_runtime::error::ProgramError::InvalidAccountData);
}
$crate::hopper_core::check::check_size(&*data, Self::LEN)?;
let verified = $crate::hopper_core::account::VerifiedAccount::from_ref(data)?;
Ok((verified, owner_idx))
}
}
impl $crate::hopper_core::check::modifier::HopperLayout for $name {
const DISC: u8 = $disc;
const VERSION: u8 = $ver;
const LAYOUT_ID: [u8; 8] = $name::LAYOUT_ID;
const LEN_WITH_HEADER: usize = $name::LEN;
}
};
}
#[macro_export]
macro_rules! hopper_check {
($account:expr, $( $constraint:tt )+) => {{
$crate::_hopper_check_inner!($account, $( $constraint )+)
}};
}
#[doc(hidden)]
#[macro_export]
macro_rules! _hopper_check_inner {
($account:expr, owner = $id:expr $(, $($rest:tt)+ )?) => {{
$crate::hopper_core::check::check_owner($account, $id)?;
$( $crate::_hopper_check_inner!($account, $($rest)+); )?
}};
($account:expr, writable $(, $($rest:tt)+ )?) => {{
$crate::hopper_core::check::check_writable($account)?;
$( $crate::_hopper_check_inner!($account, $($rest)+); )?
}};
($account:expr, signer $(, $($rest:tt)+ )?) => {{
$crate::hopper_core::check::check_signer($account)?;
$( $crate::_hopper_check_inner!($account, $($rest)+); )?
}};
($account:expr, disc = $d:expr $(, $($rest:tt)+ )?) => {{
let data = $account.try_borrow()?;
$crate::hopper_core::check::check_discriminator(&*data, $d)?;
$( $crate::_hopper_check_inner!($account, $($rest)+); )?
}};
($account:expr, size >= $n:expr $(, $($rest:tt)+ )?) => {{
let data = $account.try_borrow()?;
$crate::hopper_core::check::check_size(&*data, $n)?;
$( $crate::_hopper_check_inner!($account, $($rest)+); )?
}};
($account:expr,) => {};
}
#[macro_export]
macro_rules! hopper_error {
(
base = $base:literal;
$( $name:ident ),+ $(,)?
) => {
$crate::_hopper_error_inner!($base; $( $name ),+);
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! _hopper_error_inner {
($code:expr; $name:ident) => {
pub struct $name;
impl $name {
pub const CODE: u32 = $code;
}
impl From<$name> for $crate::hopper_runtime::error::ProgramError {
fn from(_: $name) -> $crate::hopper_runtime::error::ProgramError {
$crate::hopper_runtime::error::ProgramError::Custom($code)
}
}
};
($code:expr; $name:ident, $($rest:ident),+) => {
$crate::_hopper_error_inner!($code; $name);
$crate::_hopper_error_inner!($code + 1; $($rest),+);
};
}
#[macro_export]
macro_rules! hopper_require {
($cond:expr, $err:expr) => {
if !$cond {
return Err($err.into());
}
};
}
#[macro_export]
macro_rules! hopper_init {
($payer:expr, $account:expr, $system:expr, $program_id:expr, $layout:ty) => {{
let lamports = $crate::hopper_core::check::rent_exempt_min(<$layout>::LEN);
let space = <$layout>::LEN as u64;
$crate::hopper_system::CreateAccount {
from: $payer,
to: $account,
lamports,
space,
owner: $program_id,
}
.invoke()?;
let mut data = $account.try_borrow_mut()?;
$crate::hopper_core::account::zero_init(&mut *data);
<$layout>::write_init_header(&mut *data)
}};
}
#[macro_export]
macro_rules! hopper_close {
($account:expr, $destination:expr) => {
$crate::hopper_core::account::safe_close_with_sentinel($account, $destination)
};
}
#[macro_export]
macro_rules! hopper_register_discs {
( $( $layout:ty ),+ $(,)? ) => {
const _: () = {
let discs: &[u8] = &[ $( <$layout>::DISC, )+ ];
let names: &[&str] = &[ $( stringify!($layout), )+ ];
let n = discs.len();
let mut i = 0;
while i < n {
let mut j = i + 1;
while j < n {
assert!(
discs[i] != discs[j],
"Duplicate discriminator detected in hopper_register_discs!"
);
j += 1;
}
i += 1;
}
let _ = names; };
};
}
#[macro_export]
macro_rules! hopper_verify_pda {
($account:expr, $seeds:expr, $program_id:expr, $layout:ty) => {{
if <$layout>::has_bump_offset() {
$crate::hopper_core::check::verify_pda_cached(
$account,
$seeds,
<$layout>::BUMP_OFFSET,
$program_id,
)
} else {
match $crate::hopper_core::check::find_and_verify_pda($account, $seeds, $program_id) {
Ok(_bump) => Ok(()),
Err(e) => Err(e),
}
}
}};
}
#[macro_export]
macro_rules! hopper_invariant {
( $( $label:literal => $check:expr ),+ $(,)? ) => {{
let mut _result: $crate::hopper_runtime::ProgramResult = Ok(());
$(
if _result.is_ok() {
_result = $check;
}
)+
_result
}};
}
#[macro_export]
macro_rules! hopper_manifest {
(
$const_name:ident = $name:ident {
$( $field:ident : $fty:ty = $fsize:literal ),+ $(,)?
}
) => {
pub const $const_name: $crate::hopper_schema::LayoutManifest = {
const FIELD_COUNT: usize = 0 $( + { let _ = stringify!($field); 1 } )+;
const SIZES: [u16; FIELD_COUNT] = [ $( $fsize ),+ ];
const NAMES: [&str; FIELD_COUNT] = [ $( stringify!($field) ),+ ];
const TYPES: [&str; FIELD_COUNT] = [ $( stringify!($fty) ),+ ];
const FIELDS: [$crate::hopper_schema::FieldDescriptor; FIELD_COUNT] = {
let h = $crate::hopper_core::account::HEADER_LEN as u16;
let mut result = [$crate::hopper_schema::FieldDescriptor {
name: "", canonical_type: "", size: 0, offset: 0,
intent: $crate::hopper_schema::FieldIntent::Custom,
}; FIELD_COUNT];
let mut offset = h;
let mut i = 0;
while i < FIELD_COUNT {
result[i] = $crate::hopper_schema::FieldDescriptor {
name: NAMES[i],
canonical_type: TYPES[i],
size: SIZES[i],
offset,
intent: $crate::hopper_schema::FieldIntent::Custom,
};
offset += SIZES[i];
i += 1;
}
result
};
$crate::hopper_schema::LayoutManifest {
name: stringify!($name),
version: <$name>::VERSION,
disc: <$name>::DISC,
layout_id: <$name>::LAYOUT_ID,
total_size: <$name>::LEN,
field_count: FIELD_COUNT,
fields: &FIELDS,
}
};
};
}
#[macro_export]
macro_rules! hopper_segment {
(
$(#[$attr:meta])*
pub struct $name:ident, disc = $disc:literal
{
$( $seg:ident : $sty:ty = $ssize:literal ),+ $(,)?
}
) => {
$(#[$attr])*
pub struct $name;
impl $name {
pub const DISC: u8 = $disc;
$crate::_hopper_segment_ids!($( $seg ),+);
pub const SEGMENT_COUNT: usize = $crate::_hopper_segment_count!($( $seg ),+);
pub const TOTAL_SIZE: usize = {
let registry_size = $crate::hopper_core::account::registry::REGISTRY_HEADER_SIZE
+ (Self::SEGMENT_COUNT * $crate::hopper_core::account::registry::SEGMENT_ENTRY_SIZE);
$crate::hopper_core::account::HEADER_LEN
+ registry_size
$( + $ssize )+
};
#[inline]
pub fn init_segments(data: &mut [u8]) -> Result<(), $crate::hopper_runtime::error::ProgramError> {
let specs: &[($crate::hopper_core::account::registry::SegmentId, u32, u8)] = &[
$(
(
$crate::hopper_core::account::registry::segment_id(stringify!($seg)),
$ssize as u32,
1u8,
),
)+
];
$crate::hopper_core::account::SegmentRegistryMut::init(data, specs)
}
#[inline]
pub fn load_segment<T: $crate::hopper_core::account::Pod + $crate::hopper_core::account::FixedLayout>(
data: &[u8],
seg_id: &$crate::hopper_core::account::registry::SegmentId,
) -> Result<&T, $crate::hopper_runtime::error::ProgramError> {
let registry = $crate::hopper_core::account::SegmentRegistry::from_account(data)?;
registry.segment_overlay::<T>(seg_id)
}
#[inline]
pub fn load_segment_mut<T: $crate::hopper_core::account::Pod + $crate::hopper_core::account::FixedLayout>(
data: &mut [u8],
seg_id: &$crate::hopper_core::account::registry::SegmentId,
) -> Result<&mut T, $crate::hopper_runtime::error::ProgramError> {
let mut registry = $crate::hopper_core::account::SegmentRegistryMut::from_account_mut(data)?;
registry.segment_overlay_mut::<T>(seg_id)
}
}
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! _hopper_segment_ids {
( $( $seg:ident ),+ ) => {
$(
$crate::_hopper_segment_id_const!($seg);
)+
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! _hopper_segment_id_const {
($seg:ident) => {
#[doc = concat!("Segment ID for `", stringify!($seg), "`.")]
#[allow(non_upper_case_globals)]
pub const $seg: $crate::hopper_core::account::registry::SegmentId =
$crate::hopper_core::account::registry::segment_id(stringify!($seg));
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! _hopper_segment_count {
( $( $seg:ident ),+ ) => {
{
let mut _n = 0usize;
$( let _ = stringify!($seg); _n += 1; )+
_n
}
};
}
#[macro_export]
macro_rules! hopper_validate {
(
accounts = $accounts:expr,
program_id = $program_id:expr,
data = $data:expr,
rules {
$( $rule:expr ),+ $(,)?
}
) => {{
let _vctx = $crate::hopper_core::check::graph::ValidationContext::new(
$program_id,
$accounts,
$data,
);
$crate::hopper_core::check::graph::require_unique_writable_accounts()(&_vctx)?;
$( ($rule)(&_vctx)?; )+
Ok::<(), $crate::hopper_runtime::error::ProgramError>(())
}};
}
#[macro_export]
macro_rules! hopper_virtual {
(
slots = $n:literal,
map {
$( $slot:literal => account_index: $idx:literal
$(, owned $( = $owner:expr )? )?
$(, writable )?
),+ $(,)?
}
) => {{
let mut vs = $crate::hopper_core::virtual_state::VirtualState::<$n>::new();
$(
vs = $crate::_hopper_virtual_slot!(vs, $slot, $idx
$(, owned $( = $owner )? )?
$(, writable )?
);
)+
vs
}};
}
#[doc(hidden)]
#[macro_export]
macro_rules! _hopper_virtual_slot {
($vs:expr, $slot:literal, $idx:literal, owned, writable) => {
$vs.map_mut($slot, $idx)
};
($vs:expr, $slot:literal, $idx:literal, owned) => {
$vs.map($slot, $idx)
};
($vs:expr, $slot:literal, $idx:literal, writable) => {
$vs.set_slot(
$slot,
$crate::hopper_core::virtual_state::VirtualSlot {
account_index: $idx,
require_owned: false,
require_writable: true,
},
)
};
($vs:expr, $slot:literal, $idx:literal) => {
$vs.map_foreign($slot, $idx)
};
}
#[macro_export]
macro_rules! hopper_assert_compatible {
($old:ty, $new:ty, append) => {
const _: () = {
assert!(
<$new>::LEN > <$old>::LEN,
"New layout must be larger than old for append compatibility"
);
assert!(
<$new>::DISC == <$old>::DISC,
"Discriminator must remain the same across versions"
);
assert!(
<$new>::VERSION > <$old>::VERSION,
"New version must be strictly greater"
);
let old_id = <$old>::LAYOUT_ID;
let new_id = <$new>::LAYOUT_ID;
let mut same = true;
let mut i = 0;
while i < 8 {
if old_id[i] != new_id[i] {
same = false;
}
i += 1;
}
assert!(!same, "Layout IDs must differ between versions");
};
};
($old:ty, $new:ty, differs) => {
const _: () = {
let old_id = <$old>::LAYOUT_ID;
let new_id = <$new>::LAYOUT_ID;
let mut same = true;
let mut i = 0;
while i < 8 {
if old_id[i] != new_id[i] {
same = false;
}
i += 1;
}
assert!(!same, "Layout IDs must differ between versions");
};
};
}
#[macro_export]
macro_rules! hopper_assert_fingerprint {
($layout:ty, $expected:expr) => {
const _: () = {
let actual = <$layout>::LAYOUT_ID;
let expected: [u8; 8] = $expected;
let mut i = 0;
while i < 8 {
assert!(
actual[i] == expected[i],
"Layout fingerprint doesn't match expected value -- ABI may have changed"
);
i += 1;
}
};
};
}
pub use hopper_core;
pub use hopper_runtime;
pub use hopper_schema;
pub use hopper_system;
#[macro_export]
macro_rules! const_assert_pod {
($ty:ty, $size:expr) => {
const _: () = assert!(
core::mem::align_of::<$ty>() == 1,
concat!(
"Pod type `",
stringify!($ty),
"` must have alignment 1 for zero-copy safety. ",
"Ensure all fields use alignment-1 wire types ([u8; N], WireU64, etc.)."
)
);
const _: () = assert!(
core::mem::size_of::<$ty>() == $size,
concat!(
"Pod type `",
stringify!($ty),
"` size mismatch: ",
"expected ",
stringify!($size),
" bytes"
)
);
};
}
#[macro_export]
macro_rules! hopper_interface {
(
$(#[$attr:meta])*
pub struct $name:ident, disc = $disc:literal, version = $ver:literal
{
$(
$(#[$field_attr:meta])*
$field:ident : $fty:ty = $fsize:literal
),+ $(,)?
}
) => {
$(#[$attr])*
#[derive(Clone, Copy)]
#[repr(C)]
pub struct $name {
pub header: $crate::hopper_core::account::AccountHeader,
$(
$(#[$field_attr])*
pub $field: $fty,
)+
}
const _: () = {
let expected = $crate::hopper_core::account::HEADER_LEN $( + $fsize )+;
assert!(
core::mem::size_of::<$name>() == expected,
"Interface size mismatch: struct size != declared field sizes + header"
);
assert!(
core::mem::align_of::<$name>() == 1,
"Interface alignment must be 1 for zero-copy safety"
);
};
#[cfg(feature = "hopper-native-backend")]
unsafe impl $crate::hopper_runtime::__hopper_native::bytemuck::Zeroable for $name {}
#[cfg(feature = "hopper-native-backend")]
unsafe impl $crate::hopper_runtime::__hopper_native::bytemuck::Pod for $name {}
unsafe impl $crate::hopper_core::account::Pod for $name {}
unsafe impl $crate::hopper_runtime::__sealed::HopperZeroCopySealed for $name {}
impl $crate::hopper_core::account::FixedLayout for $name {
const SIZE: usize = $crate::hopper_core::account::HEADER_LEN $( + $fsize )+;
}
impl $crate::hopper_core::field_map::FieldMap for $name {
const FIELDS: &'static [$crate::hopper_core::field_map::FieldInfo] = {
const FIELD_COUNT: usize = 0 $( + { let _ = stringify!($field); 1 } )+;
const NAMES: [&str; FIELD_COUNT] = [ $( stringify!($field) ),+ ];
const SIZES: [usize; FIELD_COUNT] = [ $( $fsize ),+ ];
const FIELDS: [$crate::hopper_core::field_map::FieldInfo; FIELD_COUNT] = {
let mut result = [$crate::hopper_core::field_map::FieldInfo::new("", 0, 0); FIELD_COUNT];
let mut offset = $crate::hopper_core::account::HEADER_LEN;
let mut index = 0;
while index < FIELD_COUNT {
result[index] = $crate::hopper_core::field_map::FieldInfo::new(
NAMES[index],
offset,
SIZES[index],
);
offset += SIZES[index];
index += 1;
}
result
};
&FIELDS
};
}
impl $crate::hopper_runtime::LayoutContract for $name {
const DISC: u8 = $disc;
const VERSION: u8 = $ver;
const LAYOUT_ID: [u8; 8] = $name::LAYOUT_ID;
const SIZE: usize = $name::LEN;
const TYPE_OFFSET: usize = 0;
}
impl $crate::hopper_schema::SchemaExport for $name {
fn layout_manifest() -> $crate::hopper_schema::LayoutManifest {
const FIELD_COUNT: usize = 0 $( + { let _ = stringify!($field); 1 } )+;
const SIZES: [u16; FIELD_COUNT] = [ $( $fsize ),+ ];
const NAMES: [&str; FIELD_COUNT] = [ $( stringify!($field) ),+ ];
const TYPES: [&str; FIELD_COUNT] = [ $( stringify!($fty) ),+ ];
const FIELDS: [$crate::hopper_schema::FieldDescriptor; FIELD_COUNT] = {
let mut result = [$crate::hopper_schema::FieldDescriptor {
name: "", canonical_type: "", size: 0, offset: 0,
intent: $crate::hopper_schema::FieldIntent::Custom,
}; FIELD_COUNT];
let mut offset = $crate::hopper_core::account::HEADER_LEN as u16;
let mut index = 0;
while index < FIELD_COUNT {
result[index] = $crate::hopper_schema::FieldDescriptor {
name: NAMES[index],
canonical_type: TYPES[index],
size: SIZES[index],
offset,
intent: $crate::hopper_schema::FieldIntent::Custom,
};
offset += SIZES[index];
index += 1;
}
result
};
$crate::hopper_schema::LayoutManifest {
name: stringify!($name),
version: <$name>::VERSION,
disc: <$name>::DISC,
layout_id: <$name>::LAYOUT_ID,
total_size: <$name>::LEN,
field_count: FIELD_COUNT,
fields: &FIELDS,
}
}
}
impl $name {
pub const LEN: usize = $crate::hopper_core::account::HEADER_LEN $( + $fsize )+;
pub const DISC: u8 = $disc;
pub const VERSION: u8 = $ver;
pub const LAYOUT_ID: [u8; 8] = {
const INPUT: &str = concat!(
"hopper:v1:",
stringify!($name), ":",
stringify!($ver), ":",
$( stringify!($field), ":", stringify!($fty), ":", stringify!($fsize), ",", )+
);
const HASH: [u8; 32] = $crate::hopper_core::__sha256_const(INPUT.as_bytes());
[
HASH[0], HASH[1], HASH[2], HASH[3],
HASH[4], HASH[5], HASH[6], HASH[7],
]
};
#[inline(always)]
pub fn overlay(data: &[u8]) -> Result<&Self, $crate::hopper_runtime::error::ProgramError> {
$crate::hopper_core::account::pod_from_bytes::<Self>(data)
}
#[deprecated(since = "0.2.0", note = "renamed to load_cross_program()")]
#[inline]
pub fn load_foreign<'a>(
account: &'a $crate::hopper_runtime::AccountView,
expected_owner: &$crate::hopper_runtime::Address,
) -> Result<
$crate::hopper_core::account::VerifiedAccount<'a, Self>,
$crate::hopper_runtime::error::ProgramError,
> {
Self::load_cross_program(account, expected_owner)
}
#[inline]
pub fn load_cross_program<'a>(
account: &'a $crate::hopper_runtime::AccountView,
expected_owner: &$crate::hopper_runtime::Address,
) -> Result<
$crate::hopper_core::account::VerifiedAccount<'a, Self>,
$crate::hopper_runtime::error::ProgramError,
> {
$crate::hopper_core::check::check_owner(account, expected_owner)?;
let data = account.try_borrow()?;
let layout_id = $crate::hopper_core::account::read_layout_id(&*data)?;
if layout_id != Self::LAYOUT_ID {
return Err($crate::hopper_runtime::error::ProgramError::InvalidAccountData);
}
$crate::hopper_core::check::check_size(&*data, Self::LEN)?;
$crate::hopper_core::account::VerifiedAccount::from_ref(data)
}
#[inline]
pub fn load_foreign_multi<'a>(
account: &'a $crate::hopper_runtime::AccountView,
owners: &[&$crate::hopper_runtime::Address],
) -> Result<
($crate::hopper_core::account::VerifiedAccount<'a, Self>, usize),
$crate::hopper_runtime::error::ProgramError,
> {
let owner_idx = $crate::hopper_core::check::check_owner_multi(account, owners)?;
let data = account.try_borrow()?;
let layout_id = $crate::hopper_core::account::read_layout_id(&*data)?;
if layout_id != Self::LAYOUT_ID {
return Err($crate::hopper_runtime::error::ProgramError::InvalidAccountData);
}
$crate::hopper_core::check::check_size(&*data, Self::LEN)?;
let verified = $crate::hopper_core::account::VerifiedAccount::from_ref(data)?;
Ok((verified, owner_idx))
}
#[inline]
pub fn load_with_profile<'a>(
account: &'a $crate::hopper_runtime::AccountView,
profile: &$crate::hopper_core::check::trust::TrustProfile<'a>,
) -> Result<
$crate::hopper_core::account::VerifiedAccount<'a, Self>,
$crate::hopper_runtime::error::ProgramError,
> {
let data = profile.load(account)?;
$crate::hopper_core::account::VerifiedAccount::from_ref(data)
}
#[inline]
pub fn load_unverified(data: &[u8]) -> Option<(&Self, bool)> {
if data.len() < Self::LEN {
return None;
}
let validated = $crate::hopper_core::account::check_header(
data,
Self::DISC,
Self::VERSION,
&Self::LAYOUT_ID,
)
.is_ok();
let overlay = unsafe { &*(data.as_ptr() as *const Self) };
Some((overlay, validated))
}
}
};
}
#[macro_export]
macro_rules! hopper_accounts {
(
$(#[$attr:meta])*
pub struct $name:ident {
$( $field:ident : ( $($kind:tt)+ ) ),+ $(,)?
}
) => {
$crate::_hopper_accounts_struct!($name; $( $field: ($($kind)+) ; )+);
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! _hopper_accounts_struct {
($name:ident; $( $field:ident : $kind:tt ; )+) => {
pub struct $name<'a> {
$(
pub $field: $crate::_hopper_field_type!($kind),
)+
}
impl<'a> $crate::hopper_core::accounts::HopperAccounts<'a> for $name<'a> {
type Bumps = ();
const ACCOUNT_COUNT: usize = {
#[allow(unused)]
const N: usize = [$( { let _ = stringify!($field); 0u8 }, )+].len();
N
};
fn try_from_accounts(
program_id: &'a $crate::hopper_runtime::Address,
accounts: &'a [$crate::hopper_runtime::AccountView],
_instruction_data: &'a [u8],
) -> Result<(Self, Self::Bumps), $crate::hopper_runtime::error::ProgramError> {
let mut _idx: usize = 0;
$(
if _idx >= accounts.len() {
return Err($crate::hopper_runtime::error::ProgramError::NotEnoughAccountKeys);
}
let $field = $crate::_hopper_field_parse!(
&accounts[_idx], program_id, $kind
)?;
_idx += 1;
)+
Ok((Self { $( $field, )+ }, ()))
}
#[cfg(feature = "explain")]
fn context_schema() -> Option<
&'static $crate::hopper_core::accounts::explain::ContextSchema
> {
static FIELDS: &[$crate::hopper_core::accounts::explain::AccountFieldSchema] = &[
$(
$crate::hopper_core::accounts::explain::AccountFieldSchema {
name: stringify!($field),
kind: $crate::_hopper_field_kind_name!($kind),
mutable: $crate::_hopper_field_is_mut!($kind),
signer: $crate::_hopper_field_is_signer!($kind),
layout: $crate::_hopper_field_layout_name!($kind),
policy: None,
seeds: &[],
optional: false,
},
)+
];
static SCHEMA: $crate::hopper_core::accounts::explain::ContextSchema =
$crate::hopper_core::accounts::explain::ContextSchema {
name: stringify!($name),
fields: FIELDS,
policy_names: &[],
receipts_expected: false,
mutation_classes: &[],
};
Some(&SCHEMA)
}
}
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! _hopper_field_type {
((mut signer)) => { $crate::hopper_core::accounts::SignerAccount<'a> };
((signer)) => { $crate::hopper_core::accounts::SignerAccount<'a> };
((mut account < $layout:ty >)) => { $crate::hopper_core::accounts::HopperAccount<'a, $layout> };
((account < $layout:ty >)) => { $crate::hopper_core::accounts::HopperAccount<'a, $layout> };
((program)) => { $crate::hopper_core::accounts::ProgramRef<'a> };
((unchecked)) => { $crate::hopper_core::accounts::UncheckedAccount<'a> };
((mut unchecked)) => { $crate::hopper_core::accounts::UncheckedAccount<'a> };
}
#[doc(hidden)]
#[macro_export]
macro_rules! _hopper_field_parse {
($account:expr, $program_id:expr, (mut signer)) => {{
$crate::hopper_core::check::check_writable($account)?;
$crate::hopper_core::accounts::SignerAccount::from_account($account)
}};
($account:expr, $program_id:expr, (signer)) => {
$crate::hopper_core::accounts::SignerAccount::from_account($account)
};
($account:expr, $program_id:expr, (mut account < $layout:ty >)) => {
$crate::hopper_core::accounts::HopperAccount::<$layout>::from_account_mut(
$account,
$program_id,
)
};
($account:expr, $program_id:expr, (account < $layout:ty >)) => {
$crate::hopper_core::accounts::HopperAccount::<$layout>::from_account($account, $program_id)
};
($account:expr, $program_id:expr, (program)) => {
$crate::hopper_core::accounts::ProgramRef::from_account($account)
};
($account:expr, $program_id:expr, (unchecked)) => {
Ok::<_, $crate::hopper_runtime::error::ProgramError>(
$crate::hopper_core::accounts::UncheckedAccount::new($account),
)
};
($account:expr, $program_id:expr, (mut unchecked)) => {{
$crate::hopper_core::check::check_writable($account)?;
Ok::<_, $crate::hopper_runtime::error::ProgramError>(
$crate::hopper_core::accounts::UncheckedAccount::new($account),
)
}};
}
#[doc(hidden)]
#[macro_export]
macro_rules! _hopper_field_kind_name {
((mut signer)) => {
"Signer"
};
((signer)) => {
"Signer"
};
((mut account < $layout:ty >)) => {
"HopperAccount"
};
((account < $layout:ty >)) => {
"HopperAccount"
};
((program)) => {
"ProgramRef"
};
((unchecked)) => {
"Unchecked"
};
((mut unchecked)) => {
"Unchecked"
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! _hopper_field_is_mut {
((mut signer)) => {
true
};
((signer)) => {
false
};
((mut account < $layout:ty >)) => {
true
};
((account < $layout:ty >)) => {
false
};
((program)) => {
false
};
((unchecked)) => {
false
};
((mut unchecked)) => {
true
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! _hopper_field_is_signer {
((mut signer)) => {
true
};
((signer)) => {
true
};
((mut account < $layout:ty >)) => {
false
};
((account < $layout:ty >)) => {
false
};
((program)) => {
false
};
((unchecked)) => {
false
};
((mut unchecked)) => {
false
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! _hopper_field_layout_name {
((mut signer)) => {
None
};
((signer)) => {
None
};
((mut account < $layout:ty >)) => {
Some(stringify!($layout))
};
((account < $layout:ty >)) => {
Some(stringify!($layout))
};
((program)) => {
None
};
((unchecked)) => {
None
};
((mut unchecked)) => {
None
};
}