Skip to main content

hopper_runtime/
native_boundary.rs

1use crate::account::AccountView;
2use crate::address::Address;
3use crate::error::ProgramError;
4use crate::ProgramResult;
5
6pub type BackendAccountView<'info> = hopper_native::AccountView<'info>;
7pub type BackendAccountSlice<'info> = &'info [BackendAccountView<'info>];
8pub type BackendAddress = hopper_native::Address;
9pub type BackendProgramResult = hopper_native::ProgramResult;
10pub type BackendRef<'a, T> = hopper_native::borrow::Ref<'a, T>;
11pub type BackendRefMut<'a, T> = hopper_native::borrow::RefMut<'a, T>;
12pub const BACKEND_MAX_TX_ACCOUNTS: usize = hopper_native::MAX_TX_ACCOUNTS;
13pub const BACKEND_SUCCESS: u64 = hopper_native::SUCCESS;
14
15/// # Safety
16///
17/// Caller must provide the account slice handed to Hopper by the native Solana
18/// entrypoint boundary. `AccountView` is layout-checked as transparent over the
19/// native account view before this cast is used.
20#[inline(always)]
21pub unsafe fn wrap_account_slice<'info>(
22    accounts: &'info [BackendAccountView<'info>],
23) -> &'info [AccountView<'info>] {
24    // SAFETY: AccountView is repr(transparent) over BackendAccountView and
25    // compile-time layout assertions in account.rs enforce size/alignment.
26    unsafe {
27        core::slice::from_raw_parts(
28            accounts.as_ptr() as *const AccountView<'info>,
29            accounts.len(),
30        )
31    }
32}
33
34#[inline(always)]
35pub fn account_address<'a>(view: &'a BackendAccountView<'a>) -> &'a Address {
36    // SAFETY: Hopper Address and BackendAddress are both 32-byte transparent
37    // address wrappers; returned reference is tied to the backend view.
38    unsafe { &*(view.address() as *const BackendAddress as *const Address) }
39}
40
41/// # Safety
42///
43/// The returned owner reference is invalidated if the native account owner is
44/// reassigned. Callers that need stable ownership should use `read_owner`.
45#[inline(always)]
46pub unsafe fn account_owner<'a>(view: &'a BackendAccountView<'a>) -> &'a Address {
47    // SAFETY: Same address-layout cast as account_address; caller upholds the
48    // owner-reference invalidation contract documented above.
49    unsafe { &*(view.owner() as *const BackendAddress as *const Address) }
50}
51
52#[inline(always)]
53pub fn read_owner(view: &BackendAccountView<'_>) -> Address {
54    Address::from(view.read_owner())
55}
56
57#[inline(always)]
58pub fn as_backend_address(address: &Address) -> &BackendAddress {
59    // SAFETY: Address and BackendAddress share the exact 32-byte wire layout.
60    unsafe { &*(address as *const Address as *const BackendAddress) }
61}
62
63#[inline(always)]
64pub fn owned_by(view: &BackendAccountView<'_>, program: &Address) -> bool {
65    view.owned_by(as_backend_address(program))
66}
67
68#[inline(always)]
69pub fn disc(view: &BackendAccountView<'_>) -> u8 {
70    view.disc()
71}
72
73#[inline(always)]
74pub fn version(view: &BackendAccountView<'_>) -> u8 {
75    view.version()
76}
77
78#[inline(always)]
79pub fn layout_id(view: &BackendAccountView<'_>) -> Option<[u8; 8]> {
80    view.layout_id()
81}
82
83/// # Safety
84///
85/// Caller must ensure the account is writable and that owner reassignment is
86/// authorized by the active instruction.
87#[inline(always)]
88pub unsafe fn assign(view: &BackendAccountView<'_>, new_owner: &Address) {
89    // SAFETY: Caller guarantees owner reassignment is authorized; the address
90    // cast preserves the 32-byte owner value exactly.
91    unsafe {
92        view.assign(as_backend_address(new_owner));
93    }
94}
95
96/// Set an account's lamport balance.
97///
98/// This is the funnel **every** safe runtime/`hopper-core` lamport
99/// mutation crosses (`AccountView::{try_set_lamports, set_lamports,
100/// close_to, close_to_unchecked}`, lifecycle close/realloc top-up, the
101/// host System-transfer emulation, and the gated
102/// [`lamports::transfer_lamports`](crate::lamports::transfer_lamports)
103/// helper). When an instruction-scoped lamport
104/// gate is installed (`strict_writes` + declared lamport dimension,
105/// mutation-completeness contract), mutation on an undeclared account is refused here with
106/// `Custom(0xD000 | index)` before any balance changes. The gate is
107/// consulted by the account's **address value** read from the live
108/// view right here, the gate store holds copied values, no pointers.
109#[inline(always)]
110pub fn try_set_lamports(view: &BackendAccountView<'_>, lamports: u64) -> ProgramResult {
111    crate::write_policy::check_lamport_mutation(account_address(view))?;
112    view.set_lamports(lamports);
113    Ok(())
114}
115
116/// Close an account at the backend level (drains lamports to zero and
117/// wipes the header), gated by the same lamport gate as
118/// [`try_set_lamports`], the native close mutates the balance without
119/// crossing the set-lamports funnel, so it must consult the gate itself.
120#[inline(always)]
121pub fn close(view: &BackendAccountView<'_>) -> ProgramResult {
122    crate::write_policy::check_lamport_mutation(account_address(view))?;
123    view.close().map_err(ProgramError::from)
124}
125
126#[inline(always)]
127pub fn zero_data(view: &BackendAccountView<'_>) -> ProgramResult {
128    let mut data = view.try_borrow_mut().map_err(ProgramError::from)?;
129    let mut i = 0;
130    while i < data.len() {
131        data[i] = 0;
132        i += 1;
133    }
134    Ok(())
135}
136
137#[inline(always)]
138pub fn resize(view: &BackendAccountView<'_>, new_len: usize) -> ProgramResult {
139    view.resize(new_len).map_err(ProgramError::from)
140}
141
142#[inline(always)]
143pub fn resize_raw(view: &BackendAccountView<'_>, new_len: usize) -> ProgramResult {
144    view.resize_raw(new_len).map_err(ProgramError::from)
145}
146
147#[inline(always)]
148pub fn find_program_address(seeds: &[&[u8]], program_id: &Address) -> (Address, u8) {
149    let (address, bump) =
150        hopper_native::pda::find_program_address(seeds, as_backend_address(program_id));
151    (Address::from(address), bump)
152}
153
154#[inline(always)]
155pub fn create_program_address(
156    seeds: &[&[u8]],
157    program_id: &Address,
158) -> Result<Address, ProgramError> {
159    hopper_native::pda::create_program_address(seeds, as_backend_address(program_id))
160        .map(Address::from)
161        .map_err(|_| ProgramError::InvalidSeeds)
162}
163
164/// # Safety
165///
166/// Called from the exported Solana entrypoint with the loader-provided input
167/// buffer. The pointer must be the raw SVM input buffer for this invocation.
168#[inline(always)]
169pub unsafe fn process_entrypoint<const MAX: usize>(
170    input: *mut u8,
171    process_instruction: fn(
172        &BackendAddress,
173        BackendAccountSlice<'_>,
174        &[u8],
175    ) -> BackendProgramResult,
176) -> u64 {
177    // SAFETY: Entrypoint caller provides the SVM input pointer and bridge
178    // function matching Hopper Native's expected ABI.
179    unsafe { hopper_native::entrypoint::process_entrypoint::<MAX>(input, process_instruction) }
180}
181
182#[inline(always)]
183pub fn bridge_to_runtime(
184    program_id: &BackendAddress,
185    accounts: BackendAccountSlice<'_>,
186    data: &[u8],
187    process_instruction: for<'info> fn(
188        &'info Address,
189        &'info [AccountView<'info>],
190        &'info [u8],
191    ) -> ProgramResult,
192) -> BackendProgramResult {
193    // SAFETY: BackendAddress and Address have identical 32-byte layouts;
194    // lifetime is inherited from the entrypoint-provided program id.
195    let hopper_id = unsafe { &*(program_id as *const BackendAddress as *const Address) };
196    // SAFETY: Backend account slice comes directly from Hopper Native and is
197    // repr-compatible with runtime AccountView.
198    let hopper_accounts = unsafe { wrap_account_slice(accounts) };
199    match process_instruction(hopper_id, hopper_accounts, data) {
200        Ok(()) => Ok(()),
201        Err(error) => Err(error.into()),
202    }
203}
204
205impl From<BackendAddress> for Address {
206    #[inline(always)]
207    fn from(address: BackendAddress) -> Self {
208        Self(address.to_bytes())
209    }
210}
211
212impl From<Address> for BackendAddress {
213    #[inline(always)]
214    fn from(address: Address) -> Self {
215        BackendAddress::new_from_array(address.to_bytes())
216    }
217}
218
219#[doc(hidden)]
220#[macro_export]
221macro_rules! __hopper_native_entrypoint {
222    ( $process_instruction:expr, $maximum:expr ) => {
223        /// # Safety
224        ///
225        /// Called by the Solana runtime; `input` is a valid BPF input buffer.
226        #[no_mangle]
227        pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64 {
228            #[inline(always)]
229            fn __hopper_bridge(
230                program_id: &$crate::native_boundary::BackendAddress,
231                accounts: $crate::native_boundary::BackendAccountSlice<'_>,
232                data: &[u8],
233            ) -> $crate::native_boundary::BackendProgramResult {
234                $crate::native_boundary::bridge_to_runtime(
235                    program_id,
236                    accounts,
237                    data,
238                    $process_instruction,
239                )
240            }
241            // SAFETY: Solana calls this entrypoint with a valid BPF input
242            // buffer; process_entrypoint performs the loader-frame parse.
243            unsafe {
244                $crate::native_boundary::process_entrypoint::<$maximum>(input, __hopper_bridge)
245            }
246        }
247    };
248}