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}