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}