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    let ptr = unsafe { account.data_ptr_unchecked().add(offset) };
86    // SAFETY: T: Pod ⇒ align 1, every bit pattern valid, no padding.
87    // Bounds and arithmetic overflow checked above. No alignment check
88    // needed (Pod's align-1 obligation subsumes it).
89    Ok(unsafe { &*(ptr as *const T) })
90}
91
92/// Read a 32-byte address from account data.
93///
94/// The most common cross-program read: check the authority, mint, owner,
95/// or any other public key stored in a foreign account.
96#[inline]
97pub fn read_address(account: &AccountView, offset: usize) -> Result<&Address, ProgramError> {
98    let data_len = account.data_len();
99    if offset.checked_add(32).map_or(true, |end| end > data_len) {
100        return Err(ProgramError::AccountDataTooSmall);
101    }
102    let ptr = unsafe { account.data_ptr_unchecked().add(offset) };
103    // SAFETY: Address is #[repr(transparent)] over [u8; 32].
104    // Alignment 1, bounds checked above.
105    Ok(unsafe { &*(ptr as *const Address) })
106}
107
108/// Read a little-endian u64 from account data.
109///
110/// Returns the value by copy (no alignment concerns). This is the
111/// safest way to read a u64 from potentially unaligned account data --
112/// no pointer cast, just a byte copy.
113#[inline]
114pub fn read_le_u64(account: &AccountView, offset: usize) -> Result<u64, ProgramError> {
115    let data_len = account.data_len();
116    if offset.checked_add(8).map_or(true, |end| end > data_len) {
117        return Err(ProgramError::AccountDataTooSmall);
118    }
119    let ptr = unsafe { account.data_ptr_unchecked().add(offset) };
120    let mut bytes = [0u8; 8];
121    unsafe {
122        core::ptr::copy_nonoverlapping(ptr, bytes.as_mut_ptr(), 8);
123    }
124    Ok(u64::from_le_bytes(bytes))
125}
126
127/// Read a little-endian u32 from account data.
128#[inline]
129pub fn read_le_u32(account: &AccountView, offset: usize) -> Result<u32, ProgramError> {
130    let data_len = account.data_len();
131    if offset.checked_add(4).map_or(true, |end| end > data_len) {
132        return Err(ProgramError::AccountDataTooSmall);
133    }
134    let ptr = unsafe { account.data_ptr_unchecked().add(offset) };
135    let mut bytes = [0u8; 4];
136    unsafe {
137        core::ptr::copy_nonoverlapping(ptr, bytes.as_mut_ptr(), 4);
138    }
139    Ok(u32::from_le_bytes(bytes))
140}
141
142/// Read a little-endian u16 from account data.
143#[inline]
144pub fn read_le_u16(account: &AccountView, offset: usize) -> Result<u16, ProgramError> {
145    let data_len = account.data_len();
146    if offset.checked_add(2).map_or(true, |end| end > data_len) {
147        return Err(ProgramError::AccountDataTooSmall);
148    }
149    let ptr = unsafe { account.data_ptr_unchecked().add(offset) };
150    let mut bytes = [0u8; 2];
151    unsafe {
152        core::ptr::copy_nonoverlapping(ptr, bytes.as_mut_ptr(), 2);
153    }
154    Ok(u16::from_le_bytes(bytes))
155}
156
157/// Read a single byte from account data.
158#[inline]
159pub fn read_u8(account: &AccountView, offset: usize) -> Result<u8, ProgramError> {
160    if offset >= account.data_len() {
161        return Err(ProgramError::AccountDataTooSmall);
162    }
163    Ok(unsafe { *account.data_ptr_unchecked().add(offset) })
164}
165
166/// Read a boolean from account data (0 = false, nonzero = true).
167#[inline]
168pub fn read_bool(account: &AccountView, offset: usize) -> Result<bool, ProgramError> {
169    read_u8(account, offset).map(|b| b != 0)
170}
171
172/// Read a byte slice from account data.
173///
174/// Returns a reference to `len` bytes starting at `offset`.
175/// Useful for reading variable-length fields when you know the layout.
176#[inline]
177pub fn read_bytes(account: &AccountView, offset: usize, len: usize) -> Result<&[u8], ProgramError> {
178    let data_len = account.data_len();
179    if offset.checked_add(len).map_or(true, |end| end > data_len) {
180        return Err(ProgramError::AccountDataTooSmall);
181    }
182    let ptr = unsafe { account.data_ptr_unchecked().add(offset) };
183    Ok(unsafe { core::slice::from_raw_parts(ptr, len) })
184}
185
186/// Compare a field in account data against an expected value without copying.
187///
188/// Returns true if the `len` bytes at `offset` match `expected`.
189/// Useful for checking discriminators or magic numbers in foreign accounts.
190#[inline]
191pub fn field_eq(
192    account: &AccountView,
193    offset: usize,
194    expected: &[u8],
195) -> Result<bool, ProgramError> {
196    let actual = read_bytes(account, offset, expected.len())?;
197    Ok(actual == expected)
198}