hopper_native/account_view.rs
1//! RuntimeAccount memory layout and AccountView zero-copy wrapper.
2//!
3//! `RuntimeAccount` maps 1:1 onto the BPF input buffer layout that the
4//! Solana runtime writes for each account. `AccountView` is a thin
5//! pointer to a `RuntimeAccount` in that buffer, providing safe accessors
6//! for address, owner, flags, lamports, and data.
7
8use crate::address::{address_eq, Address};
9use crate::borrow::{Ref, RefMut};
10use crate::error::ProgramError;
11use crate::raw_account::RuntimeAccount;
12use crate::{ProgramResult, MAX_PERMITTED_DATA_INCREASE, NOT_BORROWED};
13
14// ── AccountView ──────────────────────────────────────────────────────
15
16/// Zero-copy view over a Solana account in the BPF input buffer.
17///
18/// `AccountView` stores a raw pointer to the `RuntimeAccount` header.
19/// All accessor methods read directly from the input buffer with no copies.
20#[repr(C)]
21#[cfg_attr(feature = "copy", derive(Copy))]
22#[derive(Clone, PartialEq, Eq)]
23pub struct AccountView {
24 raw: *mut RuntimeAccount,
25}
26
27// SAFETY: AccountView is safe to send between threads in test contexts.
28// On BPF there is only one thread.
29unsafe impl Send for AccountView {}
30unsafe impl Sync for AccountView {}
31
32impl AccountView {
33 /// Construct an AccountView from a raw pointer.
34 ///
35 /// # Safety
36 ///
37 /// `raw` must point to a valid `RuntimeAccount` in the BPF input buffer
38 /// (or a test allocation with the same layout), followed by at least
39 /// `(*raw).data_len` bytes of account data.
40 #[inline(always)]
41 pub const unsafe fn new_unchecked(raw: *mut RuntimeAccount) -> Self {
42 Self { raw }
43 }
44
45 #[inline(always)]
46 pub(crate) const fn raw_ptr(&self) -> *mut RuntimeAccount {
47 self.raw
48 }
49
50 // ── Getters ──────────────────────────────────────────────────────
51
52 /// The account's public key.
53 #[inline(always)]
54 pub fn address(&self) -> &Address {
55 // SAFETY: raw always points to a valid RuntimeAccount.
56 unsafe { &(*self.raw).address }
57 }
58
59 /// The owning program's address.
60 ///
61 /// # Safety
62 ///
63 /// The returned reference is invalidated if the account is assigned
64 /// to a new owner or closed. The caller must ensure no concurrent
65 /// mutation occurs.
66 #[inline(always)]
67 pub unsafe fn owner(&self) -> &Address {
68 // SAFETY: raw is valid; caller promises no concurrent mutation.
69 unsafe { &(*self.raw).owner }
70 }
71
72 /// Whether this account signed the transaction.
73 #[inline(always)]
74 pub fn is_signer(&self) -> bool {
75 // SAFETY: raw is valid.
76 unsafe { (*self.raw).is_signer != 0 }
77 }
78
79 /// Whether this account is writable in the transaction.
80 #[inline(always)]
81 pub fn is_writable(&self) -> bool {
82 // 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.
83 unsafe { (*self.raw).is_writable != 0 }
84 }
85
86 /// Whether this account contains an executable program.
87 #[inline(always)]
88 pub fn executable(&self) -> bool {
89 // 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.
90 unsafe { (*self.raw).executable != 0 }
91 }
92
93 /// Current data length in bytes.
94 #[inline(always)]
95 pub fn data_len(&self) -> usize {
96 // 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.
97 unsafe { (*self.raw).data_len as usize }
98 }
99
100 /// Resize delta (difference between current and original data length).
101 #[inline(always)]
102 pub fn resize_delta(&self) -> i32 {
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 unsafe { (*self.raw).resize_delta }
105 }
106
107 /// Current lamport balance.
108 #[inline(always)]
109 pub fn lamports(&self) -> u64 {
110 // 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.
111 unsafe { (*self.raw).lamports }
112 }
113
114 /// Whether the account data is empty (data_len == 0).
115 #[inline(always)]
116 pub fn is_data_empty(&self) -> bool {
117 self.data_len() == 0
118 }
119
120 /// Set the lamport balance.
121 #[inline(always)]
122 pub fn set_lamports(&self, lamports: u64) {
123 // 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.
124 unsafe {
125 (*self.raw).lamports = lamports;
126 }
127 }
128
129 // ── Ownership ────────────────────────────────────────────────────
130
131 /// Check whether this account is owned by the given program.
132 #[inline(always)]
133 pub fn owned_by(&self, program: &Address) -> bool {
134 // SAFETY: owner field is valid for the lifetime of the input buffer.
135 unsafe { address_eq(&(*self.raw).owner, program) }
136 }
137
138 /// Assign a new owner.
139 ///
140 /// # Safety
141 ///
142 /// The caller must ensure the account is writable and that ownership
143 /// transfer is authorized by the current owner program.
144 #[inline(always)]
145 pub unsafe fn assign(&self, new_owner: &Address) {
146 // 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.
147 unsafe {
148 (*self.raw).owner = new_owner.clone();
149 }
150 }
151
152 // ── Borrow tracking ─────────────────────────────────────────────
153
154 /// Whether the account data is currently borrowed (shared or exclusive).
155 #[inline(always)]
156 pub fn is_borrowed(&self) -> bool {
157 // 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.
158 unsafe { (*self.raw).borrow_state != NOT_BORROWED }
159 }
160
161 /// Whether the account data is exclusively (mutably) borrowed.
162 #[inline(always)]
163 pub fn is_borrowed_mut(&self) -> bool {
164 // 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.
165 unsafe { (*self.raw).borrow_state == 0 }
166 }
167
168 /// Check that the account can be shared-borrowed.
169 #[inline(always)]
170 pub fn check_borrow(&self) -> Result<(), ProgramError> {
171 // 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.
172 let state = unsafe { (*self.raw).borrow_state };
173 if state == 0 {
174 // Exclusively borrowed -- cannot share.
175 Err(ProgramError::AccountBorrowFailed)
176 } else {
177 Ok(())
178 }
179 }
180
181 /// Check that the account can be exclusively borrowed.
182 #[inline(always)]
183 pub fn check_borrow_mut(&self) -> Result<(), ProgramError> {
184 // 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.
185 let state = unsafe { (*self.raw).borrow_state };
186 if state != NOT_BORROWED {
187 // Already borrowed (shared or exclusive).
188 Err(ProgramError::AccountBorrowFailed)
189 } else {
190 Ok(())
191 }
192 }
193
194 // ── Unchecked data access ────────────────────────────────────────
195
196 /// Borrow account data without borrow tracking.
197 ///
198 /// # Safety
199 ///
200 /// The caller must ensure no mutable borrow is active.
201 #[inline(always)]
202 pub unsafe fn borrow_unchecked(&self) -> &[u8] {
203 let data_ptr = self.data_ptr_unchecked();
204 let len = self.data_len();
205 // 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.
206 unsafe { core::slice::from_raw_parts(data_ptr, len) }
207 }
208
209 /// Mutably borrow account data without borrow tracking.
210 ///
211 /// # Safety
212 ///
213 /// The caller must ensure no other borrows (shared or exclusive) are active.
214 #[inline(always)]
215 pub unsafe fn borrow_unchecked_mut(&self) -> &mut [u8] {
216 let data_ptr = self.data_ptr_unchecked();
217 let len = self.data_len();
218 // 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.
219 unsafe { core::slice::from_raw_parts_mut(data_ptr, len) }
220 }
221
222 // ── Checked data access ──────────────────────────────────────────
223
224 /// Try to obtain a shared borrow of the account data.
225 ///
226 /// Returns `Err(AccountBorrowFailed)` if the data is exclusively borrowed.
227 #[inline(always)]
228 pub fn try_borrow(&self) -> Result<Ref<'_, [u8]>, ProgramError> {
229 self.check_borrow()?;
230 // 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.
231 let state_ptr = unsafe { &mut (*self.raw).borrow_state as *mut u8 };
232 let state = unsafe { *state_ptr };
233 let new_state = if state == NOT_BORROWED { 1 } else { state + 1 };
234 if new_state == 0 {
235 // Overflow into exclusive-borrow sentinel.
236 return Err(ProgramError::AccountBorrowFailed);
237 }
238 // 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.
239 unsafe {
240 *state_ptr = new_state;
241 }
242 // 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.
243 let data = unsafe { self.borrow_unchecked() };
244 Ok(Ref::new(data, state_ptr))
245 }
246
247 /// Try to obtain an exclusive (mutable) borrow of the account data.
248 ///
249 /// Returns `Err(AccountBorrowFailed)` if the data is already borrowed.
250 #[inline(always)]
251 pub fn try_borrow_mut(&self) -> Result<RefMut<'_, [u8]>, ProgramError> {
252 self.check_borrow_mut()?;
253 // 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.
254 let state_ptr = unsafe { &mut (*self.raw).borrow_state as *mut u8 };
255 unsafe {
256 *state_ptr = 0;
257 } // Mark exclusive.
258 // 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.
259 let data = unsafe { self.borrow_unchecked_mut() };
260 Ok(RefMut::new(data, state_ptr))
261 }
262
263 // ── Typed segment and raw access ───────────────────────────────
264
265 /// Project a typed segment from account data with native borrow tracking.
266 #[inline(always)]
267 pub fn segment_ref<T: crate::pod::Pod>(
268 &self,
269 offset: u32,
270 size: u32,
271 ) -> Result<Ref<'_, T>, ProgramError> {
272 let expected_size = core::mem::size_of::<T>() as u32;
273 if size != expected_size {
274 return Err(ProgramError::InvalidArgument);
275 }
276
277 let end = offset
278 .checked_add(size)
279 .ok_or(ProgramError::ArithmeticOverflow)?;
280 if end as usize > self.data_len() {
281 return Err(ProgramError::AccountDataTooSmall);
282 }
283
284 self.check_borrow()?;
285 // 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.
286 let state_ptr = unsafe { &mut (*self.raw).borrow_state as *mut u8 };
287 let state = unsafe { *state_ptr };
288 let new_state = if state == NOT_BORROWED { 1 } else { state + 1 };
289 if new_state == 0 {
290 return Err(ProgramError::AccountBorrowFailed);
291 }
292 // 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.
293 unsafe {
294 *state_ptr = new_state;
295 }
296
297 // 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.
298 let ptr = unsafe { self.data_ptr_unchecked().add(offset as usize) as *const T };
299 Ok(Ref::new(unsafe { &*ptr }, state_ptr))
300 }
301
302 /// Acquire a shared segment borrow without size/bounds validation.
303 ///
304 /// # Safety
305 ///
306 /// The caller must have already verified:
307 /// - `offset + size_of::<T>()` does not overflow
308 /// - `offset + size_of::<T>() <= data_len()`
309 #[inline(always)]
310 pub unsafe fn segment_ref_unchecked<T: crate::pod::Pod>(
311 &self,
312 offset: u32,
313 ) -> Result<Ref<'_, T>, ProgramError> {
314 self.check_borrow()?;
315 // 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.
316 let state_ptr = unsafe { &mut (*self.raw).borrow_state as *mut u8 };
317 let state = unsafe { *state_ptr };
318 let new_state = if state == NOT_BORROWED { 1 } else { state + 1 };
319 if new_state == 0 {
320 return Err(ProgramError::AccountBorrowFailed);
321 }
322 // 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.
323 unsafe {
324 *state_ptr = new_state;
325 }
326
327 // 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.
328 let ptr = unsafe { self.data_ptr_unchecked().add(offset as usize) as *const T };
329 Ok(Ref::new(unsafe { &*ptr }, state_ptr))
330 }
331
332 /// Project a mutable typed segment from account data with native borrow tracking.
333 #[inline(always)]
334 pub fn segment_mut<T: crate::pod::Pod>(
335 &self,
336 offset: u32,
337 size: u32,
338 ) -> Result<RefMut<'_, T>, ProgramError> {
339 self.require_writable()?;
340
341 let expected_size = core::mem::size_of::<T>() as u32;
342 if size != expected_size {
343 return Err(ProgramError::InvalidArgument);
344 }
345
346 let end = offset
347 .checked_add(size)
348 .ok_or(ProgramError::ArithmeticOverflow)?;
349 if end as usize > self.data_len() {
350 return Err(ProgramError::AccountDataTooSmall);
351 }
352
353 self.check_borrow_mut()?;
354 // 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.
355 let state_ptr = unsafe { &mut (*self.raw).borrow_state as *mut u8 };
356 unsafe {
357 *state_ptr = 0;
358 }
359
360 // 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.
361 let ptr = unsafe { self.data_ptr_unchecked().add(offset as usize) as *mut T };
362 Ok(RefMut::new(unsafe { &mut *ptr }, state_ptr))
363 }
364
365 /// Acquire an exclusive segment borrow without size/bounds/writable validation.
366 ///
367 /// # Safety
368 ///
369 /// The caller must have already verified:
370 /// - The account is writable
371 /// - `offset + size_of::<T>()` does not overflow
372 /// - `offset + size_of::<T>() <= data_len()`
373 #[inline(always)]
374 pub unsafe fn segment_mut_unchecked<T: crate::pod::Pod>(
375 &self,
376 offset: u32,
377 ) -> Result<RefMut<'_, T>, ProgramError> {
378 self.check_borrow_mut()?;
379 // 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.
380 let state_ptr = unsafe { &mut (*self.raw).borrow_state as *mut u8 };
381 unsafe {
382 *state_ptr = 0;
383 }
384
385 // 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.
386 let ptr = unsafe { self.data_ptr_unchecked().add(offset as usize) as *mut T };
387 Ok(RefMut::new(unsafe { &mut *ptr }, state_ptr))
388 }
389
390 /// Explicit raw typed read of the account buffer.
391 #[inline(always)]
392 ///
393 /// # Safety
394 ///
395 /// Caller must uphold the invariants documented for this unsafe API before invoking it.
396 pub unsafe fn raw_ref<T: crate::pod::Pod>(&self) -> Result<Ref<'_, T>, ProgramError> {
397 self.segment_ref::<T>(0, core::mem::size_of::<T>() as u32)
398 }
399
400 /// Explicit raw typed write of the account buffer.
401 #[inline(always)]
402 ///
403 /// # Safety
404 ///
405 /// Caller must uphold the invariants documented for this unsafe API before invoking it.
406 pub unsafe fn raw_mut<T: crate::pod::Pod>(&self) -> Result<RefMut<'_, T>, ProgramError> {
407 self.segment_mut::<T>(0, core::mem::size_of::<T>() as u32)
408 }
409
410 // ── Resize ───────────────────────────────────────────────────────
411
412 /// Resize the account data to `new_len` bytes.
413 ///
414 /// Returns `Err(InvalidRealloc)` if the new length exceeds the
415 /// permitted increase from the original allocation.
416 #[inline(always)]
417 pub fn resize(&self, new_len: usize) -> Result<(), ProgramError> {
418 let original_len = (self.data_len() as i64 - self.resize_delta() as i64) as usize;
419 if new_len > original_len + MAX_PERMITTED_DATA_INCREASE {
420 return Err(ProgramError::InvalidRealloc);
421 }
422 let delta = new_len as i64 - original_len as i64;
423 // 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.
424 unsafe {
425 (*self.raw).data_len = new_len as u64;
426 (*self.raw).resize_delta = delta as i32;
427 }
428 Ok(())
429 }
430
431 /// Resize without bounds checking.
432 ///
433 /// # Safety
434 ///
435 /// The caller must guarantee `new_len <= original_len + MAX_PERMITTED_DATA_INCREASE`.
436 #[inline(always)]
437 pub unsafe fn resize_unchecked(&self, new_len: usize) {
438 let original_len = (self.data_len() as i64 - self.resize_delta() as i64) as usize;
439 let delta = new_len as i64 - original_len as i64;
440 // 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.
441 unsafe {
442 (*self.raw).data_len = new_len as u64;
443 (*self.raw).resize_delta = delta as i32;
444 }
445 }
446
447 // ── Close ────────────────────────────────────────────────────────
448
449 /// Solana System Program address (all-zero pubkey).
450 ///
451 /// Closing an account transfers ownership back to the System
452 /// Program, which is the canonical "no-owner" state on Solana.
453 /// The byte value `[0u8; 32]` and `Address::default()` are
454 /// equivalent, but using this named constant makes the intent
455 /// explicit, per the Hopper Safety Audit which flagged the
456 /// `Address::default()` spelling as documentation drift.
457 pub const SYSTEM_PROGRAM_ID: Address = Address::new_from_array([0u8; 32]);
458
459 /// Close the account: zero lamports and data, reassign owner to
460 /// the System Program.
461 ///
462 /// # Caveat
463 ///
464 /// This low-level routine does **not** verify the caller has
465 /// authority to close the account, Solana's runtime enforces
466 /// owner/writable rules at transaction commit time regardless, but
467 /// higher-level APIs (e.g. `hopper_runtime::AccountView::close_to`)
468 /// should pre-check those rules. See `account.rs::close_to` for
469 /// the safe wrapper.
470 #[inline(always)]
471 pub fn close(&self) -> ProgramResult {
472 self.set_lamports(0);
473 // 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.
474 unsafe {
475 let len = self.data_len();
476 if len > 0 {
477 // Use the SVM's JIT-compiled memset for optimal CU cost.
478 crate::mem::memset(self.data_ptr_unchecked(), 0, len);
479 }
480 (*self.raw).data_len = 0;
481 (*self.raw).owner = Self::SYSTEM_PROGRAM_ID;
482 }
483 Ok(())
484 }
485
486 /// Close without borrow checks.
487 ///
488 /// # Safety
489 ///
490 /// The caller must ensure no active borrows exist.
491 #[inline(always)]
492 pub unsafe fn close_unchecked(&self) {
493 // 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.
494 unsafe {
495 (*self.raw).lamports = 0;
496 (*self.raw).data_len = 0;
497 (*self.raw).owner = Self::SYSTEM_PROGRAM_ID;
498 }
499 }
500
501 // ── Raw pointers ─────────────────────────────────────────────────
502
503 /// Raw pointer to the `RuntimeAccount` header.
504 #[inline(always)]
505 pub const fn account_ptr(&self) -> *const RuntimeAccount {
506 self.raw as *const RuntimeAccount
507 }
508
509 /// Raw pointer to the first byte of account data.
510 ///
511 /// The data starts immediately after the 88-byte `RuntimeAccount` header.
512 /// This is an expert-only substrate escape hatch: constructing the pointer
513 /// is safe, but dereferencing it is unsafe and bypasses Hopper Native's
514 /// borrow-state checks, segment registry, and writable checks. Normal code
515 /// should use `try_borrow`, `try_borrow_mut`, `segment_ref`, or
516 /// `segment_mut`. Framework code should route user-facing raw access
517 /// through the documented unsafe runtime APIs (`Context::as_mut_ptr` /
518 /// `Context::as_ptr`) instead of exposing this method directly.
519 #[doc(hidden)]
520 #[inline(always)]
521 pub fn data_ptr_unchecked(&self) -> *mut u8 {
522 // SAFETY: Adding the struct size to the base pointer yields the
523 // first data byte. The runtime guarantees this memory is valid.
524 unsafe { (self.raw as *mut u8).add(core::mem::size_of::<RuntimeAccount>()) }
525 }
526
527 // ── Hopper Innovations ───────────────────────────────────────────
528
529 /// Validate that this account is a signer, returning a typed error.
530 #[inline(always)]
531 pub fn require_signer(&self) -> ProgramResult {
532 if self.is_signer() {
533 Ok(())
534 } else {
535 Err(ProgramError::MissingRequiredSignature)
536 }
537 }
538
539 /// Validate that this account is writable.
540 #[inline(always)]
541 pub fn require_writable(&self) -> ProgramResult {
542 if self.is_writable() {
543 Ok(())
544 } else {
545 Err(ProgramError::Immutable)
546 }
547 }
548
549 /// Validate that this account is owned by the given program.
550 #[inline(always)]
551 pub fn require_owned_by(&self, program: &Address) -> ProgramResult {
552 if self.owned_by(program) {
553 Ok(())
554 } else {
555 Err(ProgramError::IncorrectProgramId)
556 }
557 }
558
559 /// Validate signer + writable (common "payer" pattern).
560 #[inline(always)]
561 pub fn require_payer(&self) -> ProgramResult {
562 self.require_signer()?;
563 self.require_writable()
564 }
565
566 /// Read the Hopper account discriminator (first byte of data).
567 ///
568 /// Returns 0 if the account has no data.
569 #[inline(always)]
570 pub fn disc(&self) -> u8 {
571 if self.data_len() == 0 {
572 return 0;
573 }
574 // 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.
575 unsafe { *self.data_ptr_unchecked() }
576 }
577
578 /// Read the Hopper account version (second byte of data).
579 ///
580 /// Returns 0 if the account has fewer than 2 bytes.
581 #[inline(always)]
582 pub fn version(&self) -> u8 {
583 if self.data_len() < 2 {
584 return 0;
585 }
586 // 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.
587 unsafe { *self.data_ptr_unchecked().add(1) }
588 }
589
590 /// Read the 8-byte layout_id from the Hopper account header
591 /// (bytes 4..12 of account data, per the canonical header format).
592 ///
593 /// Returns `None` if the account has fewer than 12 bytes.
594 #[inline(always)]
595 pub fn layout_id(&self) -> Option<&[u8; 8]> {
596 if self.data_len() < 12 {
597 return None;
598 }
599 // 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.
600 unsafe { Some(&*(self.data_ptr_unchecked().add(4) as *const [u8; 8])) }
601 }
602
603 /// Verify that this account has the given discriminator.
604 #[inline(always)]
605 pub fn require_disc(&self, expected: u8) -> ProgramResult {
606 if self.disc() == expected {
607 Ok(())
608 } else {
609 Err(ProgramError::InvalidAccountData)
610 }
611 }
612
613 // -- Chainable validation (Steel-inspired, improved) ---------------
614 //
615 // Return `Result<&Self>` so callers can chain:
616 //
617 // account
618 // .check_signer()?
619 // .check_writable()?
620 // .check_owned_by(&MY_PROGRAM_ID)?;
621 //
622 // Validated once, used everywhere. This pattern exists in Steel but
623 // not in pinocchio, Anchor, or Quasar.
624
625 /// Chainable signer check.
626 #[inline(always)]
627 pub fn check_signer(&self) -> Result<&Self, ProgramError> {
628 if self.is_signer() {
629 Ok(self)
630 } else {
631 Err(ProgramError::MissingRequiredSignature)
632 }
633 }
634
635 /// Chainable writable check.
636 #[inline(always)]
637 pub fn check_writable(&self) -> Result<&Self, ProgramError> {
638 if self.is_writable() {
639 Ok(self)
640 } else {
641 Err(ProgramError::Immutable)
642 }
643 }
644
645 /// Chainable ownership check.
646 #[inline(always)]
647 pub fn check_owned_by(&self, program: &Address) -> Result<&Self, ProgramError> {
648 if self.owned_by(program) {
649 Ok(self)
650 } else {
651 Err(ProgramError::IncorrectProgramId)
652 }
653 }
654
655 /// Chainable discriminator check.
656 #[inline(always)]
657 pub fn check_disc(&self, expected: u8) -> Result<&Self, ProgramError> {
658 if self.disc() == expected {
659 Ok(self)
660 } else {
661 Err(ProgramError::InvalidAccountData)
662 }
663 }
664
665 /// Chainable non-empty data check.
666 #[inline(always)]
667 pub fn check_has_data(&self) -> Result<&Self, ProgramError> {
668 if !self.is_data_empty() {
669 Ok(self)
670 } else {
671 Err(ProgramError::AccountDataTooSmall)
672 }
673 }
674
675 /// Chainable executable check.
676 #[inline(always)]
677 pub fn check_executable(&self) -> Result<&Self, ProgramError> {
678 if self.executable() {
679 Ok(self)
680 } else {
681 Err(ProgramError::InvalidArgument)
682 }
683 }
684
685 /// Chainable address check.
686 #[inline(always)]
687 pub fn check_address(&self, expected: &Address) -> Result<&Self, ProgramError> {
688 if address_eq(self.address(), expected) {
689 Ok(self)
690 } else {
691 Err(ProgramError::InvalidArgument)
692 }
693 }
694
695 /// Chainable minimum data length check.
696 #[inline(always)]
697 pub fn check_data_len(&self, min_len: usize) -> Result<&Self, ProgramError> {
698 if self.data_len() >= min_len {
699 Ok(self)
700 } else {
701 Err(ProgramError::AccountDataTooSmall)
702 }
703 }
704
705 // -- Safe owner access ---------------------------------------------
706
707 /// Read the owner address as a copy (32-byte value).
708 ///
709 /// Unlike `owner()` (which is unsafe due to reference invalidation
710 /// if `assign()` is called), this returns a copy that is always safe.
711 /// Costs 32 bytes of stack space but eliminates aliasing hazards.
712 #[inline(always)]
713 pub fn read_owner(&self) -> Address {
714 // 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.
715 unsafe { (*self.raw).owner.clone() }
716 }
717
718 // -- Packed flags --------------------------------------------------
719
720 /// Read the first 4 bytes of the account header as a single u32.
721 ///
722 /// Layout (little-endian): `[borrow_state, is_signer, is_writable, executable]`
723 ///
724 /// This is the fastest way to extract multiple account properties at once
725 ///, a single aligned u32 read instead of 3-4 separate byte loads.
726 #[inline(always)]
727 fn header_u32(&self) -> u32 {
728 // SAFETY: RuntimeAccount is #[repr(C)] with first 4 fields as u8,
729 // totalling 4 bytes at the start. Reading as u32 is safe because
730 // the struct is at least 88 bytes and the BPF input buffer is
731 // sufficiently aligned.
732 // 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.
733 unsafe { *(self.raw as *const u32) }
734 }
735
736 /// Pack the account's boolean flags into a single byte for fast
737 /// comparison.
738 ///
739 /// Bit layout:
740 /// - bit 0: is_signer
741 /// - bit 1: is_writable
742 /// - bit 2: executable
743 /// - bit 3: has data (data_len > 0)
744 ///
745 /// Use with `expect_flags()` for single-instruction multi-check:
746 ///
747 /// ```ignore
748 /// // Require: signer + writable + has data
749 /// account.expect_flags(0b1011)?;
750 /// ```
751 #[inline(always)]
752 pub fn flags(&self) -> u8 {
753 // Single u32 read extracts [borrow_state, is_signer, is_writable, executable].
754 // On little-endian: is_signer = bits 8-15, is_writable = bits 16-23, executable = bits 24-31.
755 let h = self.header_u32();
756 let mut f: u8 = 0;
757 if h & 0x0000_FF00 != 0 {
758 f |= 0b0001;
759 } // is_signer
760 if h & 0x00FF_0000 != 0 {
761 f |= 0b0010;
762 } // is_writable
763 if h & 0xFF00_0000 != 0 {
764 f |= 0b0100;
765 } // executable
766 if !self.is_data_empty() {
767 f |= 0b1000;
768 }
769 f
770 }
771
772 /// Check that the account's flags contain all the required bits.
773 ///
774 /// `required` is a bitmask of flags that must be set. See `flags()`.
775 #[inline(always)]
776 pub fn expect_flags(&self, required: u8) -> ProgramResult {
777 if self.flags() & required == required {
778 Ok(())
779 } else {
780 Err(ProgramError::InvalidArgument)
781 }
782 }
783}
784
785impl core::fmt::Debug for AccountView {
786 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
787 f.debug_struct("AccountView")
788 .field("address", self.address())
789 .field("lamports", &self.lamports())
790 .field("data_len", &self.data_len())
791 .field("is_signer", &self.is_signer())
792 .field("is_writable", &self.is_writable())
793 .finish()
794 }
795}
796
797// ── RemainingAccounts ────────────────────────────────────────────────
798
799/// Iterator over remaining (unstructured) accounts after the known ones.
800pub struct RemainingAccounts<'a> {
801 accounts: &'a [AccountView],
802 cursor: usize,
803}
804
805impl<'a> RemainingAccounts<'a> {
806 /// Create from a slice of the remaining accounts.
807 #[inline(always)]
808 pub fn new(accounts: &'a [AccountView]) -> Self {
809 Self {
810 accounts,
811 cursor: 0,
812 }
813 }
814
815 /// Number of accounts remaining.
816 #[inline(always)]
817 pub fn remaining(&self) -> usize {
818 self.accounts.len() - self.cursor
819 }
820
821 /// Take the next account, or return `NotEnoughAccountKeys`.
822 #[inline(always)]
823 pub fn next(&mut self) -> Result<&'a AccountView, ProgramError> {
824 if self.cursor >= self.accounts.len() {
825 return Err(ProgramError::NotEnoughAccountKeys);
826 }
827 let account = &self.accounts[self.cursor];
828 self.cursor += 1;
829 Ok(account)
830 }
831
832 /// Take the next account that is a signer.
833 #[inline(always)]
834 pub fn next_signer(&mut self) -> Result<&'a AccountView, ProgramError> {
835 let account = self.next()?;
836 account.require_signer()?;
837 Ok(account)
838 }
839
840 /// Take the next account that is writable.
841 #[inline(always)]
842 pub fn next_writable(&mut self) -> Result<&'a AccountView, ProgramError> {
843 let account = self.next()?;
844 account.require_writable()?;
845 Ok(account)
846 }
847
848 /// Take the next account owned by the given program.
849 #[inline(always)]
850 pub fn next_owned_by(&mut self, program: &Address) -> Result<&'a AccountView, ProgramError> {
851 let account = self.next()?;
852 account.require_owned_by(program)?;
853 Ok(account)
854 }
855}