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 from a Hopper account another program owns.
23//! // The body starts after the 16-byte header.
24//! let authority = lens::read_address(oracle_account, 16)?;
25//!
26//! // Read a u64 price at offset 48.
27//! let price = lens::read_le_u64(oracle_account, 48)?;
28//!
29//! // Read a typed struct at an offset.
30//! let data = lens::read_field::<MyPodType>(account, 16)?;
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 are checked at runtime. Alignment is settled at compile time:
62/// `Pod` promises alignment 1, and a hand-written `Pod` impl that breaks
63/// the promise fails to build here instead of reading misaligned.
64///
65/// Use this in cross-program readers that want the checked projection
66/// contract without dropping down to hand-written pointer arithmetic.
67///
68/// # Example
69///
70/// ```ignore
71/// use hopper_native::{lens, wire::LeU64};
72/// let counter = lens::read_field_pod::<LeU64>(foreign_account, 16)?;
73/// ```
74#[inline]
75pub fn read_field_pod<'a, T: crate::Pod>(
76    account: &'a AccountView<'a>,
77    offset: usize,
78) -> Result<Ref<'a, T>, ProgramError> {
79    const {
80        assert!(
81            core::mem::align_of::<T>() == 1,
82            "read_field_pod: a Pod type must have alignment 1"
83        );
84    }
85    let data_len = account.data_len();
86    let size = core::mem::size_of::<T>();
87    let end = offset
88        .checked_add(size)
89        .ok_or(ProgramError::ArithmeticOverflow)?;
90    if end > data_len {
91        return Err(ProgramError::AccountDataTooSmall);
92    }
93    // SAFETY: bounds checked above; the sum stays within the data region.
94    let ptr = unsafe { account.data_ptr_unchecked().add(offset) };
95    // Shared data borrow: released by the guard's drop; prevents coexistence
96    // with an exclusive borrow of the same region.
97    let state_ptr = account.acquire_shared()?;
98    // Bounds and arithmetic overflow checked above. No alignment check
99    // needed (Pod's align-1 obligation subsumes it).
100    // SAFETY: `ptr` is within account data bounds, `T: Pod` guarantees
101    // alignment-1 + any-bit-pattern validity, and the returned guard's
102    // lifetime is tied to `account`.
103    Ok(Ref::new(unsafe { &*(ptr as *const T) }, state_ptr))
104}
105
106/// Read a 32-byte address from account data.
107///
108/// The most common cross-program read: check the authority, mint, owner,
109/// or any other public key stored in a foreign account.
110#[inline]
111pub fn read_address<'a>(
112    account: &'a AccountView<'a>,
113    offset: usize,
114) -> Result<Ref<'a, Address>, ProgramError> {
115    let data_len = account.data_len();
116    if offset.checked_add(32).is_none_or(|end| end > data_len) {
117        return Err(ProgramError::AccountDataTooSmall);
118    }
119    // SAFETY: bounds checked above; the sum stays within the data region.
120    let ptr = unsafe { account.data_ptr_unchecked().add(offset) };
121    // Shared data borrow: released by the guard's drop.
122    let state_ptr = account.acquire_shared()?;
123    // SAFETY: Address is #[repr(transparent)] over [u8; 32].
124    // Alignment 1, bounds checked above.
125    Ok(Ref::new(unsafe { &*(ptr as *const Address) }, state_ptr))
126}
127
128/// Read a little-endian u64 from account data.
129///
130/// Returns the value by copy (no alignment concerns). This is the
131/// safest way to read a u64 from potentially unaligned account data --
132/// no pointer cast, just a byte copy.
133#[inline]
134pub fn read_le_u64(account: &AccountView<'_>, offset: usize) -> Result<u64, ProgramError> {
135    let data_len = account.data_len();
136    if offset.checked_add(8).is_none_or(|end| end > data_len) {
137        return Err(ProgramError::AccountDataTooSmall);
138    }
139    // A by-value read still reads the bytes: refuse it while someone
140    // holds them exclusively.
141    account.check_borrow()?;
142    // SAFETY: `offset + 8 <= data_len` was checked above and no exclusive
143    // borrow is live (`check_borrow`), so the eight bytes are readable. They
144    // are copied out; no reference into the account is formed.
145    let ptr = unsafe { account.data_ptr_unchecked().add(offset) };
146    let mut bytes = [0u8; 8];
147    // SAFETY: `ptr` is readable for 8 bytes (above); `bytes` is a local of that size.
148    unsafe {
149        core::ptr::copy_nonoverlapping(ptr, bytes.as_mut_ptr(), 8);
150    }
151    Ok(u64::from_le_bytes(bytes))
152}
153
154/// Read a little-endian u32 from account data.
155#[inline]
156pub fn read_le_u32(account: &AccountView<'_>, offset: usize) -> Result<u32, ProgramError> {
157    let data_len = account.data_len();
158    if offset.checked_add(4).is_none_or(|end| end > data_len) {
159        return Err(ProgramError::AccountDataTooSmall);
160    }
161    // A by-value read still reads the bytes: refuse it while someone
162    // holds them exclusively.
163    account.check_borrow()?;
164    // SAFETY: `offset + 4 <= data_len` was checked above and no exclusive
165    // borrow is live (`check_borrow`), so the four bytes are readable. They
166    // are copied out; no reference into the account is formed.
167    let ptr = unsafe { account.data_ptr_unchecked().add(offset) };
168    let mut bytes = [0u8; 4];
169    // SAFETY: `ptr` is readable for 4 bytes (above); `bytes` is a local of that size.
170    unsafe {
171        core::ptr::copy_nonoverlapping(ptr, bytes.as_mut_ptr(), 4);
172    }
173    Ok(u32::from_le_bytes(bytes))
174}
175
176/// Read a little-endian u16 from account data.
177#[inline]
178pub fn read_le_u16(account: &AccountView<'_>, offset: usize) -> Result<u16, ProgramError> {
179    let data_len = account.data_len();
180    if offset.checked_add(2).is_none_or(|end| end > data_len) {
181        return Err(ProgramError::AccountDataTooSmall);
182    }
183    // A by-value read still reads the bytes: refuse it while someone
184    // holds them exclusively.
185    account.check_borrow()?;
186    // SAFETY: `offset + 2 <= data_len` was checked above and no exclusive
187    // borrow is live (`check_borrow`), so the two bytes are readable. They
188    // are copied out; no reference into the account is formed.
189    let ptr = unsafe { account.data_ptr_unchecked().add(offset) };
190    let mut bytes = [0u8; 2];
191    // SAFETY: `ptr` is readable for 2 bytes (above); `bytes` is a local of that size.
192    unsafe {
193        core::ptr::copy_nonoverlapping(ptr, bytes.as_mut_ptr(), 2);
194    }
195    Ok(u16::from_le_bytes(bytes))
196}
197
198/// Read a single byte from account data.
199#[inline]
200pub fn read_u8(account: &AccountView<'_>, offset: usize) -> Result<u8, ProgramError> {
201    if offset >= account.data_len() {
202        return Err(ProgramError::AccountDataTooSmall);
203    }
204    account.check_borrow()?;
205    // SAFETY: `offset < data_len` was checked above and no exclusive borrow
206    // is live (`check_borrow`). The byte is copied out.
207    Ok(unsafe { *account.data_ptr_unchecked().add(offset) })
208}
209
210/// Read a boolean from account data (0 = false, nonzero = true).
211#[inline]
212pub fn read_bool(account: &AccountView<'_>, offset: usize) -> Result<bool, ProgramError> {
213    read_u8(account, offset).map(|b| b != 0)
214}
215
216/// Read a byte slice from account data.
217///
218/// Returns a reference to `len` bytes starting at `offset`.
219/// Useful for reading variable-length fields when you know the layout.
220#[inline]
221pub fn read_bytes<'a>(
222    account: &'a AccountView<'a>,
223    offset: usize,
224    len: usize,
225) -> Result<Ref<'a, [u8]>, ProgramError> {
226    let data_len = account.data_len();
227    if offset.checked_add(len).is_none_or(|end| end > data_len) {
228        return Err(ProgramError::AccountDataTooSmall);
229    }
230    // SAFETY: bounds checked above; the sum stays within the data region.
231    let ptr = unsafe { account.data_ptr_unchecked().add(offset) };
232    // Shared data borrow: released by the guard's drop.
233    let state_ptr = account.acquire_shared()?;
234    // SAFETY: bounds checked; u8 has no alignment or validity requirements.
235    Ok(Ref::new(
236        unsafe { core::slice::from_raw_parts(ptr, len) },
237        state_ptr,
238    ))
239}
240
241/// Compare a field in account data against an expected value without copying.
242///
243/// Returns true if the `len` bytes at `offset` match `expected`.
244/// Useful for checking discriminators or magic numbers in foreign accounts.
245#[inline]
246pub fn field_eq(
247    account: &AccountView<'_>,
248    offset: usize,
249    expected: &[u8],
250) -> Result<bool, ProgramError> {
251    let actual = read_bytes(account, offset, expected.len())?;
252    Ok(&*actual == expected)
253}