Skip to main content

hopper_native/
lens.rs

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