Skip to main content

hopper_native/
lens.rs

1//! Cross-program account lenses -- read foreign fields by offset.
2//!
3//! Hopper lenses read fields from foreign account data by byte offset and type
4//! without importing the foreign program's Rust type. This reduces compile-time
5//! coupling when the caller already has a reviewed layout contract.
6//!
7//! # Safety
8//!
9//! Lenses bypass type-level layout guarantees. The caller must know the
10//! correct offset and type for the target field. Incorrect offsets read
11//! garbage data, never out-of-bounds memory: every accessor is
12//! bounds-checked. Reference-returning lenses additionally hold a shared
13//! data borrow (a [`crate::borrow::Ref`] guard) for their lifetime, so
14//! they cannot coexist with an exclusive borrow of the same account's
15//! data; by-value lenses copy through raw pointers and take no borrow.
16//!
17//! # Usage
18//!
19//! ```ignore
20//! use hopper_native::lens;
21//!
22//! // Read a 32-byte address at offset 10 from a foreign program's account
23//! // (skip 10-byte Hopper header: disc + version + layout_id).
24//! let authority = lens::read_address(oracle_account, 10)?;
25//!
26//! // Read a u64 price at offset 42.
27//! let price = lens::read_le_u64(oracle_account, 42)?;
28//!
29//! // Read a typed struct at an offset.
30//! let data: &MyPodType = lens::read_field::<MyPodType>(account, 10)?;
31//! ```
32
33use crate::account_view::AccountView;
34use crate::address::Address;
35use crate::borrow::Ref;
36use crate::error::ProgramError;
37use crate::project::Projectable;
38
39/// Read a `Projectable` field from account data at the given byte offset.
40///
41/// **Tier-C escape hatch.** `Projectable`
42/// only requires `Copy + 'static`, which is too permissive to protect
43/// against padding/alignment bugs. New code should prefer
44/// [`read_field_pod`] which enforces the stronger [`crate::Pod`]
45/// bound at the type level.
46///
47/// The returned guard holds a shared data borrow for its lifetime.
48#[inline]
49pub fn read_field<'a, T: Projectable>(
50    account: &'a AccountView<'a>,
51    offset: usize,
52) -> Result<Ref<'a, T>, ProgramError> {
53    crate::project::project::<T>(account, offset, None)
54}
55
56/// Read a `Pod` field from account data at the given byte offset.
57///
58/// This is the Safety-Audit-compliant lens: requires the substrate
59/// [`crate::Pod`] bound, so the compiler rejects types with padding,
60/// non-alignment-1 fields, or forbidden bit patterns at the call site.
61/// Bounds and alignment are still checked at runtime, just as in the
62/// generic [`read_field`] escape hatch.
63///
64/// Use this in cross-program readers that want the checked projection
65/// contract without dropping down to hand-written pointer arithmetic.
66///
67/// # Example
68///
69/// ```ignore
70/// use hopper_native::{lens, wire::LeU64};
71/// let counter: &LeU64 = lens::read_field_pod(foreign_account, 16)?;
72/// ```
73#[inline]
74pub fn read_field_pod<'a, T: crate::Pod>(
75    account: &'a AccountView<'a>,
76    offset: usize,
77) -> Result<Ref<'a, T>, ProgramError> {
78    let data_len = account.data_len();
79    let size = core::mem::size_of::<T>();
80    let end = offset
81        .checked_add(size)
82        .ok_or(ProgramError::ArithmeticOverflow)?;
83    if end > data_len {
84        return Err(ProgramError::AccountDataTooSmall);
85    }
86    // SAFETY: bounds checked above; the sum stays within the data region.
87    let ptr = unsafe { account.data_ptr_unchecked().add(offset) };
88    // Shared data borrow: released by the guard's drop; prevents coexistence
89    // with an exclusive borrow of the same region.
90    let state_ptr = account.acquire_shared()?;
91    // Bounds and arithmetic overflow checked above. No alignment check
92    // needed (Pod's align-1 obligation subsumes it).
93    // SAFETY: `ptr` is within account data bounds, `T: Pod` guarantees
94    // alignment-1 + any-bit-pattern validity, and the returned guard's
95    // lifetime is tied to `account`.
96    Ok(Ref::new(unsafe { &*(ptr as *const T) }, state_ptr))
97}
98
99/// Read a 32-byte address from account data.
100///
101/// The most common cross-program read: check the authority, mint, owner,
102/// or any other public key stored in a foreign account.
103#[inline]
104pub fn read_address<'a>(
105    account: &'a AccountView<'a>,
106    offset: usize,
107) -> Result<Ref<'a, Address>, ProgramError> {
108    let data_len = account.data_len();
109    if offset.checked_add(32).is_none_or(|end| end > data_len) {
110        return Err(ProgramError::AccountDataTooSmall);
111    }
112    // SAFETY: bounds checked above; the sum stays within the data region.
113    let ptr = unsafe { account.data_ptr_unchecked().add(offset) };
114    // Shared data borrow: released by the guard's drop.
115    let state_ptr = account.acquire_shared()?;
116    // SAFETY: Address is #[repr(transparent)] over [u8; 32].
117    // Alignment 1, bounds checked above.
118    Ok(Ref::new(unsafe { &*(ptr as *const Address) }, state_ptr))
119}
120
121/// Read a little-endian u64 from account data.
122///
123/// Returns the value by copy (no alignment concerns). This is the
124/// safest way to read a u64 from potentially unaligned account data --
125/// no pointer cast, just a byte copy.
126#[inline]
127pub fn read_le_u64(account: &AccountView<'_>, offset: usize) -> Result<u64, ProgramError> {
128    let data_len = account.data_len();
129    if offset.checked_add(8).is_none_or(|end| end > data_len) {
130        return Err(ProgramError::AccountDataTooSmall);
131    }
132    // SAFETY: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
133    let ptr = unsafe { account.data_ptr_unchecked().add(offset) };
134    let mut bytes = [0u8; 8];
135    unsafe {
136        core::ptr::copy_nonoverlapping(ptr, bytes.as_mut_ptr(), 8);
137    }
138    Ok(u64::from_le_bytes(bytes))
139}
140
141/// Read a little-endian u32 from account data.
142#[inline]
143pub fn read_le_u32(account: &AccountView<'_>, offset: usize) -> Result<u32, ProgramError> {
144    let data_len = account.data_len();
145    if offset.checked_add(4).is_none_or(|end| end > data_len) {
146        return Err(ProgramError::AccountDataTooSmall);
147    }
148    // SAFETY: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
149    let ptr = unsafe { account.data_ptr_unchecked().add(offset) };
150    let mut bytes = [0u8; 4];
151    unsafe {
152        core::ptr::copy_nonoverlapping(ptr, bytes.as_mut_ptr(), 4);
153    }
154    Ok(u32::from_le_bytes(bytes))
155}
156
157/// Read a little-endian u16 from account data.
158#[inline]
159pub fn read_le_u16(account: &AccountView<'_>, offset: usize) -> Result<u16, ProgramError> {
160    let data_len = account.data_len();
161    if offset.checked_add(2).is_none_or(|end| end > data_len) {
162        return Err(ProgramError::AccountDataTooSmall);
163    }
164    // SAFETY: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
165    let ptr = unsafe { account.data_ptr_unchecked().add(offset) };
166    let mut bytes = [0u8; 2];
167    unsafe {
168        core::ptr::copy_nonoverlapping(ptr, bytes.as_mut_ptr(), 2);
169    }
170    Ok(u16::from_le_bytes(bytes))
171}
172
173/// Read a single byte from account data.
174#[inline]
175pub fn read_u8(account: &AccountView<'_>, offset: usize) -> Result<u8, ProgramError> {
176    if offset >= account.data_len() {
177        return Err(ProgramError::AccountDataTooSmall);
178    }
179    // SAFETY: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
180    Ok(unsafe { *account.data_ptr_unchecked().add(offset) })
181}
182
183/// Read a boolean from account data (0 = false, nonzero = true).
184#[inline]
185pub fn read_bool(account: &AccountView<'_>, offset: usize) -> Result<bool, ProgramError> {
186    read_u8(account, offset).map(|b| b != 0)
187}
188
189/// Read a byte slice from account data.
190///
191/// Returns a reference to `len` bytes starting at `offset`.
192/// Useful for reading variable-length fields when you know the layout.
193#[inline]
194pub fn read_bytes<'a>(
195    account: &'a AccountView<'a>,
196    offset: usize,
197    len: usize,
198) -> Result<Ref<'a, [u8]>, ProgramError> {
199    let data_len = account.data_len();
200    if offset.checked_add(len).is_none_or(|end| end > data_len) {
201        return Err(ProgramError::AccountDataTooSmall);
202    }
203    // SAFETY: bounds checked above; the sum stays within the data region.
204    let ptr = unsafe { account.data_ptr_unchecked().add(offset) };
205    // Shared data borrow: released by the guard's drop.
206    let state_ptr = account.acquire_shared()?;
207    // SAFETY: bounds checked; u8 has no alignment or validity requirements.
208    Ok(Ref::new(
209        unsafe { core::slice::from_raw_parts(ptr, len) },
210        state_ptr,
211    ))
212}
213
214/// Compare a field in account data against an expected value without copying.
215///
216/// Returns true if the `len` bytes at `offset` match `expected`.
217/// Useful for checking discriminators or magic numbers in foreign accounts.
218#[inline]
219pub fn field_eq(
220    account: &AccountView<'_>,
221    offset: usize,
222    expected: &[u8],
223) -> Result<bool, ProgramError> {
224    let actual = read_bytes(account, offset, expected.len())?;
225    Ok(&*actual == expected)
226}