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/// Both accounts must be writable and distinct. Borrow conflicts and credit
14/// overflow are rejected before either balance changes, even if the caller
15/// catches the error. The caller must verify that the executing program owns
16/// the source and that the application authorizes closure.
17#[inline]
18pub fn close_and_transfer(
19    source: &AccountView<'_>,
20    destination: &AccountView<'_>,
21) -> ProgramResult {
22    if crate::address::address_eq(source.address(), destination.address()) {
23        return Err(ProgramError::InvalidArgument);
24    }
25    source.require_writable()?;
26    destination.require_writable()?;
27    source.check_borrow_mut()?;
28    let lamports = source.lamports();
29    let credited = destination
30        .lamports()
31        .checked_add(lamports)
32        .ok_or(ProgramError::ArithmeticOverflow)?;
33    // Every fallible precondition is checked before either balance changes.
34    // Closing cannot encounter a new borrow: there is no intervening CPI or callback.
35    source.close()?;
36    destination.set_lamports(credited);
37    Ok(())
38}
39
40/// Transfer `amount` lamports between two accounts without CPI.
41///
42/// For accounts owned by the current program, direct lamport
43/// manipulation is cheaper than a system program CPI transfer.
44/// This method checks both writable flags, sufficient balance, and overflow.
45/// A same-address transfer is balance-checked net zero. The caller must verify
46/// ownership and application authority to debit the source.
47///
48/// # Gated programs (`strict_writes` + `lamports(...)`)
49///
50/// This substrate helper writes balances directly at the native layer
51/// and **bypasses the runtime's lamport gate by design**; it is the
52/// cheap no-CPI path and sits outside hopper-runtime's governed
53/// surface. Under a context that declares `strict_writes` +
54/// `lamports(...)` (the mutation-complete contract), use
55/// `hopper_runtime::transfer_lamports` instead, also reachable via
56/// `hopper::prelude` and as the generated `ctx.transfer_lamports(..)`
57/// bound-context method: identical arithmetic, but both sides cross
58/// the gated `native_boundary` funnel, so the mutation-complete
59/// guarantee covers the move.
60#[inline]
61pub fn transfer_lamports(
62    from: &AccountView<'_>,
63    to: &AccountView<'_>,
64    amount: u64,
65) -> ProgramResult {
66    from.require_writable()?;
67    to.require_writable()?;
68    let from_lamports = from.lamports();
69    if from_lamports < amount {
70        return Err(ProgramError::InsufficientFunds);
71    }
72    if crate::address::address_eq(from.address(), to.address()) {
73        return Ok(());
74    }
75    let to_lamports = to.lamports();
76    let new_to = to_lamports
77        .checked_add(amount)
78        .ok_or(ProgramError::ArithmeticOverflow)?;
79
80    from.set_lamports(from_lamports - amount);
81    to.set_lamports(new_to);
82    Ok(())
83}
84
85/// Verify that an account is rent-exempt using the **hardcoded** current
86/// rent constants (the fast, syscall-free path).
87///
88/// # SAFETY-CRITICAL caveat
89///
90/// This gates on [`crate::sysvar::rent_exempt_minimum`], which cannot see a
91/// rent *reprice*. If the cluster has raised rent, this can report an account
92/// as rent-exempt when the runtime would reap it. For any decision where a
93/// wrong "exempt" answer risks data loss, prefer
94/// [`require_rent_exempt_with`], which reads the live
95/// [`crate::sysvar::Rent`] sysvar.
96#[inline]
97pub fn require_rent_exempt(account: &AccountView<'_>) -> ProgramResult {
98    let min = crate::sysvar::rent_exempt_minimum(account.data_len());
99    if account.lamports() >= min {
100        Ok(())
101    } else {
102        Err(ProgramError::AccountNotRentExempt)
103    }
104}
105
106/// Verify that an account is rent-exempt against a live [`crate::sysvar::Rent`] sysvar
107/// (RECOMMENDED for reaping-relevant checks).
108///
109/// The caller reads the sysvar once (`Rent::get()`) and passes it in, so this
110/// function adds no syscall of its own, the cost stays where the caller can
111/// see it, while using the cluster's *actual* rent parameters. This is the
112/// correct form when the cluster may have repriced rent since the constants
113/// baked into [`require_rent_exempt`] were set: it uses
114/// [`crate::sysvar::Rent::minimum_balance`], which byte-matches the runtime.
115///
116/// # Example
117///
118/// ```ignore
119/// let rent = hopper::sysvar::Rent::get()?;
120/// hopper::batch::require_rent_exempt_with(&rent, account)?;
121/// ```
122#[inline]
123pub fn require_rent_exempt_with(
124    rent: &crate::sysvar::Rent,
125    account: &AccountView<'_>,
126) -> ProgramResult {
127    let min = rent.minimum_balance(account.data_len());
128    if account.lamports() >= min {
129        Ok(())
130    } else {
131        Err(ProgramError::AccountNotRentExempt)
132    }
133}
134
135/// Assert that two accounts have the same address.
136///
137/// Useful for verifying expected accounts match (e.g., token mint
138/// matches the vault's expected mint).
139#[inline]
140pub fn require_same_address(a: &AccountView<'_>, b: &AccountView<'_>) -> ProgramResult {
141    if crate::address::address_eq(a.address(), b.address()) {
142        Ok(())
143    } else {
144        Err(ProgramError::InvalidArgument)
145    }
146}
147
148/// Assert that an account's address matches an expected address.
149#[inline]
150pub fn require_address(account: &AccountView<'_>, expected: &Address) -> ProgramResult {
151    if crate::address::address_eq(account.address(), expected) {
152        Ok(())
153    } else {
154        Err(ProgramError::InvalidArgument)
155    }
156}
157
158/// Assert that an account has the expected discriminator AND is owned
159/// by the given program. This two-check combo is the most common
160/// "is this the right account type?" pattern in Solana programs.
161#[inline]
162pub fn require_account_type(
163    account: &AccountView<'_>,
164    expected_disc: u8,
165    expected_owner: &Address,
166) -> ProgramResult {
167    if account.disc() != expected_disc {
168        return Err(ProgramError::InvalidAccountData);
169    }
170    account.require_owned_by(expected_owner)
171}
172
173/// Zero the data bytes of an account without changing lamports or owner.
174///
175/// Useful for "soft close" patterns where you want to mark an account
176/// as consumed but leave it allocated for potential reuse.
177///
178/// Fails with `AccountBorrowFailed` while any data borrow is outstanding
179/// (zeroing would mutate memory a live `Ref`/`RefMut` still points at).
180#[inline]
181pub fn zero_data(account: &AccountView<'_>) -> ProgramResult {
182    // Delegate to the borrow-guarded, SVM-memset-optimized helper rather
183    // than duplicating an unguarded byte loop here.
184    crate::mem::zero_account_data(account)
185}
186
187/// Checked realloc that also ensures the account remains rent-exempt
188/// after resizing.
189///
190/// This is the safe version of `account.resize()` -- it verifies that
191/// the account has enough lamports to cover rent at the new data length.
192///
193/// # Reaping caveat
194///
195/// The top-up target comes from the hardcoded
196/// [`crate::sysvar::rent_exempt_minimum`] const, so it cannot see a rent
197/// reprice. If the cluster ever raises the rent parameters this
198/// UNDER-funds the account, leaving it reapable (data loss). Any resize
199/// whose safety must survive a reprice should call
200/// [`realloc_checked_with`] with a freshly read [`crate::sysvar::Rent`].
201#[inline]
202pub fn realloc_checked(
203    account: &AccountView<'_>,
204    new_len: usize,
205    payer: Option<&AccountView<'_>>,
206) -> ProgramResult {
207    // Check rent requirement BEFORE resizing to avoid leaving the account
208    // in an inconsistent state if the payer transfer fails, and check the
209    // resize preconditions BEFORE the transfer so a refused resize (not
210    // writable, live borrow, over the growth limit) cannot leave the
211    // top-up behind.
212    account.check_resize(new_len)?;
213    let min = crate::sysvar::rent_exempt_minimum(new_len);
214    let current = account.lamports();
215
216    if current < min {
217        // Need more lamports. Transfer BEFORE resize so that if the
218        // transfer fails, the account data length is unchanged.
219        if let Some(payer) = payer {
220            if crate::address::address_eq(account.address(), payer.address()) {
221                return Err(ProgramError::InvalidArgument);
222            }
223            let deficit = min - current;
224            transfer_lamports(payer, account, deficit)?;
225        } else {
226            return Err(ProgramError::AccountNotRentExempt);
227        }
228    }
229
230    // Now resize -- the account already has enough lamports.
231    account.resize(new_len)
232}
233
234/// Reaping-safe `realloc_checked`: tops the account up to rent-exemption
235/// using the **live [`Rent`] sysvar**, so it stays correct after a rent
236/// reprice.
237///
238/// [`realloc_checked`] computes its top-up from the hardcoded
239/// [`crate::sysvar::rent_exempt_minimum`] const, which cannot see a
240/// reprice and would UNDER-fund the account (leaving it reapable, data
241/// lost) if the cluster ever raised `lamports_per_byte_year` or the
242/// exemption threshold. Any resize whose correctness must survive a
243/// reprice should call this variant with a freshly read sysvar:
244///
245/// ```ignore
246/// let rent = hopper::sysvar::Rent::get()?;
247/// hopper::batch::realloc_checked_with(&rent, account, new_len, Some(payer))?;
248/// ```
249///
250/// [`Rent`]: crate::sysvar::Rent
251///
252/// The optional payer is debited directly and must be owned by the executing
253/// program. An underfunded target cannot pay itself. For a System-owned wallet
254/// or PDA payer, use `ResizeWithPayer` (feature `cpi`) instead.
255#[inline]
256pub fn realloc_checked_with(
257    rent: &crate::sysvar::Rent,
258    account: &AccountView<'_>,
259    new_len: usize,
260    payer: Option<&AccountView<'_>>,
261) -> ProgramResult {
262    // Top-up computed from the live sysvar, not the const snapshot.
263    // Check rent BEFORE resizing so a failed payer transfer leaves the
264    // account's data length unchanged, and the resize preconditions BEFORE
265    // the transfer (same ordering as realloc_checked).
266    account.check_resize(new_len)?;
267    let min = rent.minimum_balance(new_len);
268    let current = account.lamports();
269
270    if current < min {
271        if let Some(payer) = payer {
272            if crate::address::address_eq(account.address(), payer.address()) {
273                return Err(ProgramError::InvalidArgument);
274            }
275            let deficit = min - current;
276            transfer_lamports(payer, account, deficit)?;
277        } else {
278            return Err(ProgramError::AccountNotRentExempt);
279        }
280    }
281
282    account.resize(new_len)
283}
284
285/// Resize program-owned state, funding only missing rent through System CPI.
286///
287/// The default invocation reads live rent. Growth is checked against the length
288/// at instruction entry before a payer is charged. Newly exposed bytes are zeroed;
289/// shrinking retains excess lamports in the account. This does not authorize an
290/// application's resize: validate its authority before invoking this builder.
291/// `program_id` must be the current entrypoint's program ID.
292#[cfg(feature = "cpi")]
293pub struct ResizeWithPayer<'a, 'info> {
294    pub account: &'a AccountView<'info>,
295    pub payer: &'a AccountView<'info>,
296    pub system_program: &'a AccountView<'info>,
297    pub program_id: &'a Address,
298    pub new_len: usize,
299}
300
301#[cfg(feature = "cpi")]
302impl ResizeWithPayer<'_, '_> {
303    /// Fund from a transaction signer using the live Rent sysvar.
304    #[inline]
305    pub fn invoke(&self) -> ProgramResult {
306        self.invoke_signed(&[])
307    }
308
309    /// Also support a System-owned PDA payer derived by the current program.
310    /// Nonempty signer seeds are verified by the SVM, not assumed valid here.
311    #[inline]
312    pub fn invoke_signed(&self, signers: &[crate::instruction::Signer<'_, '_>]) -> ProgramResult {
313        self.invoke_signed_with_rent(&crate::sysvar::Rent::get()?, signers)
314    }
315
316    /// Reuse a Rent value already read from the target cluster in this instruction.
317    /// A fabricated or stale rent value can underfund the account; use `invoke`
318    /// unless the caller already has the live sysvar.
319    #[inline]
320    pub fn invoke_signed_with_rent(
321        &self,
322        rent: &crate::sysvar::Rent,
323        signers: &[crate::instruction::Signer<'_, '_>],
324    ) -> ProgramResult {
325        self.account.require_owned_by(self.program_id)?;
326        self.account.require_writable()?;
327        self.account.check_resize(self.new_len)?;
328        let deficit = rent
329            .minimum_balance(self.new_len)
330            .saturating_sub(self.account.lamports());
331        if deficit > 0 {
332            if crate::address::address_eq(self.account.address(), self.payer.address()) {
333                return Err(ProgramError::InvalidArgument);
334            }
335            require_address(self.system_program, &AccountView::SYSTEM_PROGRAM_ID)?;
336            if !self.system_program.executable() {
337                return Err(ProgramError::IncorrectProgramId);
338            }
339            self.payer
340                .require_owned_by(&AccountView::SYSTEM_PROGRAM_ID)?;
341            if !self.payer.is_data_empty() {
342                return Err(ProgramError::InvalidAccountData);
343            }
344            crate::system::Transfer {
345                from: self.payer,
346                to: self.account,
347                lamports: deficit,
348            }
349            .invoke_signed(signers)?;
350        }
351        self.account.resize(self.new_len)
352    }
353}