hopper_native/batch.rs
1//! Batch account operations.
2//!
3//! Common multi-account patterns as single methods with clearer intent
4//! and fewer repeated unsafe blocks. These are operations that every
5//! serious Solana program needs but nobody bundles at the substrate level.
6
7use crate::account_view::AccountView;
8use crate::address::Address;
9use crate::error::ProgramError;
10use crate::ProgramResult;
11
12/// Transfer all lamports from `source` to `destination` and zero the source.
13///
14/// This is the standard "close an account" pattern: move all SOL to
15/// the rent receiver and wipe the source account. Combines what would
16/// normally be 3 separate operations (read lamports, set source to 0,
17/// add to destination) into one safe call.
18#[inline]
19pub fn close_and_transfer(source: &AccountView, destination: &AccountView) -> ProgramResult {
20 let lamports = source.lamports();
21 if lamports == 0 {
22 // Already empty -- just close.
23 source.close()?;
24 return Ok(());
25 }
26
27 // Move lamports.
28 destination.set_lamports(
29 destination
30 .lamports()
31 .checked_add(lamports)
32 .ok_or(ProgramError::ArithmeticOverflow)?,
33 );
34
35 // Close source (zeros data, sets owner to system program).
36 source.close()
37}
38
39/// Transfer `amount` lamports between two accounts without CPI.
40///
41/// For accounts owned by the current program, direct lamport
42/// manipulation is cheaper than a system program CPI transfer.
43/// This method checks for sufficient balance and overflow.
44#[inline]
45pub fn transfer_lamports(from: &AccountView, to: &AccountView, amount: u64) -> ProgramResult {
46 let from_lamports = from.lamports();
47 if from_lamports < amount {
48 return Err(ProgramError::InsufficientFunds);
49 }
50 let to_lamports = to.lamports();
51 let new_to = to_lamports
52 .checked_add(amount)
53 .ok_or(ProgramError::ArithmeticOverflow)?;
54
55 from.set_lamports(from_lamports - amount);
56 to.set_lamports(new_to);
57 Ok(())
58}
59
60/// Verify that an account is rent-exempt under Solana's current rent constants.
61#[inline]
62pub fn require_rent_exempt(account: &AccountView) -> ProgramResult {
63 let min = crate::sysvar::rent_exempt_minimum(account.data_len());
64 if account.lamports() >= min {
65 Ok(())
66 } else {
67 Err(ProgramError::AccountNotRentExempt)
68 }
69}
70
71/// Assert that two accounts have the same address.
72///
73/// Useful for verifying expected accounts match (e.g., token mint
74/// matches the vault's expected mint).
75#[inline]
76pub fn require_same_address(a: &AccountView, b: &AccountView) -> ProgramResult {
77 if crate::address::address_eq(a.address(), b.address()) {
78 Ok(())
79 } else {
80 Err(ProgramError::InvalidArgument)
81 }
82}
83
84/// Assert that an account's address matches an expected address.
85#[inline]
86pub fn require_address(account: &AccountView, expected: &Address) -> ProgramResult {
87 if crate::address::address_eq(account.address(), expected) {
88 Ok(())
89 } else {
90 Err(ProgramError::InvalidArgument)
91 }
92}
93
94/// Assert that an account has the expected discriminator AND is owned
95/// by the given program. This two-check combo is the most common
96/// "is this the right account type?" pattern in Solana programs.
97#[inline]
98pub fn require_account_type(
99 account: &AccountView,
100 expected_disc: u8,
101 expected_owner: &Address,
102) -> ProgramResult {
103 if account.disc() != expected_disc {
104 return Err(ProgramError::InvalidAccountData);
105 }
106 account.require_owned_by(expected_owner)
107}
108
109/// Zero the data bytes of an account without changing lamports or owner.
110///
111/// Useful for "soft close" patterns where you want to mark an account
112/// as consumed but leave it allocated for potential reuse.
113#[inline]
114pub fn zero_data(account: &AccountView) -> ProgramResult {
115 let len = account.data_len();
116 if len == 0 {
117 return Ok(());
118 }
119 let data_ptr = account.data_ptr_unchecked();
120 // 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.
121 unsafe {
122 core::ptr::write_bytes(data_ptr, 0, len);
123 }
124 Ok(())
125}
126
127/// Checked realloc that also ensures the account remains rent-exempt
128/// after resizing.
129///
130/// This is the safe version of `account.resize()` -- it verifies that
131/// the account has enough lamports to cover rent at the new data length.
132#[inline]
133pub fn realloc_checked(
134 account: &AccountView,
135 new_len: usize,
136 payer: Option<&AccountView>,
137) -> ProgramResult {
138 // Check rent requirement BEFORE resizing to avoid leaving the account
139 // in an inconsistent state if the payer transfer fails.
140 let min = crate::sysvar::rent_exempt_minimum(new_len);
141 let current = account.lamports();
142
143 if current < min {
144 // Need more lamports. Transfer BEFORE resize so that if the
145 // transfer fails, the account data length is unchanged.
146 if let Some(payer) = payer {
147 let deficit = min - current;
148 transfer_lamports(payer, account, deficit)?;
149 } else {
150 return Err(ProgramError::AccountNotRentExempt);
151 }
152 }
153
154 // Now resize -- the account already has enough lamports.
155 account.resize(new_len)
156}