Skip to main content

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.
5
6use crate::account_view::AccountView;
7use crate::address::Address;
8use crate::error::ProgramError;
9use crate::ProgramResult;
10
11/// Transfer all lamports from `source` to `destination` and zero the source.
12///
13/// This is the standard "close an account" pattern: move all SOL to
14/// the rent receiver and wipe the source account. Combines what would
15/// normally be 3 separate operations (read lamports, set source to 0,
16/// add to destination) into one safe call.
17#[inline]
18pub fn close_and_transfer(
19    source: &AccountView<'_>,
20    destination: &AccountView<'_>,
21) -> ProgramResult {
22    let lamports = source.lamports();
23    if lamports == 0 {
24        // Already empty -- just close.
25        source.close()?;
26        return Ok(());
27    }
28
29    // Move lamports.
30    destination.set_lamports(
31        destination
32            .lamports()
33            .checked_add(lamports)
34            .ok_or(ProgramError::ArithmeticOverflow)?,
35    );
36
37    // Close source (zeros data, sets owner to system program).
38    source.close()
39}
40
41/// Transfer `amount` lamports between two accounts without CPI.
42///
43/// For accounts owned by the current program, direct lamport
44/// manipulation is cheaper than a system program CPI transfer.
45/// This method checks for sufficient balance and overflow.
46///
47/// # Gated programs (`strict_writes` + `lamports(...)`)
48///
49/// This substrate helper writes balances directly at the native layer
50/// and **bypasses the runtime's lamport gate by design**; it is the
51/// cheap no-CPI path and sits outside hopper-runtime's governed
52/// surface. Under a context that declares `strict_writes` +
53/// `lamports(...)` (the mutation-complete contract), use
54/// `hopper_runtime::transfer_lamports` instead, also reachable via
55/// `hopper::prelude` and as the generated `ctx.transfer_lamports(..)`
56/// bound-context method: identical arithmetic, but both sides cross
57/// the gated `native_boundary` funnel, so the mutation-complete
58/// guarantee covers the move.
59#[inline]
60pub fn transfer_lamports(
61    from: &AccountView<'_>,
62    to: &AccountView<'_>,
63    amount: u64,
64) -> ProgramResult {
65    let from_lamports = from.lamports();
66    if from_lamports < amount {
67        return Err(ProgramError::InsufficientFunds);
68    }
69    let to_lamports = to.lamports();
70    let new_to = to_lamports
71        .checked_add(amount)
72        .ok_or(ProgramError::ArithmeticOverflow)?;
73
74    from.set_lamports(from_lamports - amount);
75    to.set_lamports(new_to);
76    Ok(())
77}
78
79/// Verify that an account is rent-exempt using the **hardcoded** current
80/// rent constants (the fast, syscall-free path).
81///
82/// # SAFETY-CRITICAL caveat
83///
84/// This gates on [`crate::sysvar::rent_exempt_minimum`], which cannot see a
85/// rent *reprice*. If the cluster has raised rent, this can report an account
86/// as rent-exempt when the runtime would reap it. For any decision where a
87/// wrong "exempt" answer risks data loss, prefer
88/// [`require_rent_exempt_with`], which reads the live
89/// [`crate::sysvar::Rent`] sysvar.
90#[inline]
91pub fn require_rent_exempt(account: &AccountView<'_>) -> ProgramResult {
92    let min = crate::sysvar::rent_exempt_minimum(account.data_len());
93    if account.lamports() >= min {
94        Ok(())
95    } else {
96        Err(ProgramError::AccountNotRentExempt)
97    }
98}
99
100/// Verify that an account is rent-exempt against a live [`crate::sysvar::Rent`] sysvar
101/// (RECOMMENDED for reaping-relevant checks).
102///
103/// The caller reads the sysvar once (`Rent::get()`) and passes it in, so this
104/// function adds no syscall of its own, the cost stays where the caller can
105/// see it, while using the cluster's *actual* rent parameters. This is the
106/// correct form when the cluster may have repriced rent since the constants
107/// baked into [`require_rent_exempt`] were set: it uses
108/// [`crate::sysvar::Rent::minimum_balance`], which byte-matches the runtime.
109///
110/// # Example
111///
112/// ```ignore
113/// let rent = hopper::sysvar::Rent::get()?;
114/// hopper::batch::require_rent_exempt_with(&rent, account)?;
115/// ```
116#[inline]
117pub fn require_rent_exempt_with(
118    rent: &crate::sysvar::Rent,
119    account: &AccountView<'_>,
120) -> ProgramResult {
121    let min = rent.minimum_balance(account.data_len());
122    if account.lamports() >= min {
123        Ok(())
124    } else {
125        Err(ProgramError::AccountNotRentExempt)
126    }
127}
128
129/// Assert that two accounts have the same address.
130///
131/// Useful for verifying expected accounts match (e.g., token mint
132/// matches the vault's expected mint).
133#[inline]
134pub fn require_same_address(a: &AccountView<'_>, b: &AccountView<'_>) -> ProgramResult {
135    if crate::address::address_eq(a.address(), b.address()) {
136        Ok(())
137    } else {
138        Err(ProgramError::InvalidArgument)
139    }
140}
141
142/// Assert that an account's address matches an expected address.
143#[inline]
144pub fn require_address(account: &AccountView<'_>, expected: &Address) -> ProgramResult {
145    if crate::address::address_eq(account.address(), expected) {
146        Ok(())
147    } else {
148        Err(ProgramError::InvalidArgument)
149    }
150}
151
152/// Assert that an account has the expected discriminator AND is owned
153/// by the given program. This two-check combo is the most common
154/// "is this the right account type?" pattern in Solana programs.
155#[inline]
156pub fn require_account_type(
157    account: &AccountView<'_>,
158    expected_disc: u8,
159    expected_owner: &Address,
160) -> ProgramResult {
161    if account.disc() != expected_disc {
162        return Err(ProgramError::InvalidAccountData);
163    }
164    account.require_owned_by(expected_owner)
165}
166
167/// Zero the data bytes of an account without changing lamports or owner.
168///
169/// Useful for "soft close" patterns where you want to mark an account
170/// as consumed but leave it allocated for potential reuse.
171///
172/// Fails with `AccountBorrowFailed` while any data borrow is outstanding
173/// (zeroing would mutate memory a live `Ref`/`RefMut` still points at).
174#[inline]
175pub fn zero_data(account: &AccountView<'_>) -> ProgramResult {
176    // Delegate to the borrow-guarded, SVM-memset-optimized helper rather
177    // than duplicating an unguarded byte loop here.
178    crate::mem::zero_account_data(account)
179}
180
181/// Checked realloc that also ensures the account remains rent-exempt
182/// after resizing.
183///
184/// This is the safe version of `account.resize()` -- it verifies that
185/// the account has enough lamports to cover rent at the new data length.
186///
187/// # Reaping caveat
188///
189/// The top-up target comes from the hardcoded
190/// [`crate::sysvar::rent_exempt_minimum`] const, so it cannot see a rent
191/// reprice. If the cluster ever raises the rent parameters this
192/// UNDER-funds the account, leaving it reapable (data loss). Any resize
193/// whose safety must survive a reprice should call
194/// [`realloc_checked_with`] with a freshly read [`crate::sysvar::Rent`].
195#[inline]
196pub fn realloc_checked(
197    account: &AccountView<'_>,
198    new_len: usize,
199    payer: Option<&AccountView<'_>>,
200) -> ProgramResult {
201    // Check rent requirement BEFORE resizing to avoid leaving the account
202    // in an inconsistent state if the payer transfer fails, and check the
203    // resize preconditions BEFORE the transfer so a refused resize (not
204    // writable, live borrow, over the growth limit) cannot leave the
205    // top-up behind.
206    account.check_resize(new_len)?;
207    let min = crate::sysvar::rent_exempt_minimum(new_len);
208    let current = account.lamports();
209
210    if current < min {
211        // Need more lamports. Transfer BEFORE resize so that if the
212        // transfer fails, the account data length is unchanged.
213        if let Some(payer) = payer {
214            let deficit = min - current;
215            transfer_lamports(payer, account, deficit)?;
216        } else {
217            return Err(ProgramError::AccountNotRentExempt);
218        }
219    }
220
221    // Now resize -- the account already has enough lamports.
222    account.resize(new_len)
223}
224
225/// Reaping-safe `realloc_checked`: tops the account up to rent-exemption
226/// using the **live [`Rent`] sysvar**, so it stays correct after a rent
227/// reprice.
228///
229/// [`realloc_checked`] computes its top-up from the hardcoded
230/// [`crate::sysvar::rent_exempt_minimum`] const, which cannot see a
231/// reprice and would UNDER-fund the account (leaving it reapable, data
232/// lost) if the cluster ever raised `lamports_per_byte_year` or the
233/// exemption threshold. Any resize whose correctness must survive a
234/// reprice should call this variant with a freshly read sysvar:
235///
236/// ```ignore
237/// let rent = hopper::sysvar::Rent::get()?;
238/// hopper::batch::realloc_checked_with(&rent, account, new_len, Some(payer))?;
239/// ```
240///
241/// [`Rent`]: crate::sysvar::Rent
242#[inline]
243pub fn realloc_checked_with(
244    rent: &crate::sysvar::Rent,
245    account: &AccountView<'_>,
246    new_len: usize,
247    payer: Option<&AccountView<'_>>,
248) -> ProgramResult {
249    // Top-up computed from the live sysvar, not the const snapshot.
250    // Check rent BEFORE resizing so a failed payer transfer leaves the
251    // account's data length unchanged, and the resize preconditions BEFORE
252    // the transfer (same ordering as realloc_checked).
253    account.check_resize(new_len)?;
254    let min = rent.minimum_balance(new_len);
255    let current = account.lamports();
256
257    if current < min {
258        if let Some(payer) = payer {
259            let deficit = min - current;
260            transfer_lamports(payer, account, deficit)?;
261        } else {
262            return Err(ProgramError::AccountNotRentExempt);
263        }
264    }
265
266    account.resize(new_len)
267}