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. 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 given the current rent parameters.
61///
62/// Reads the Rent sysvar and checks that the account's lamports meet
63/// the minimum balance for its data length.
64#[inline]
65pub fn require_rent_exempt(account: &AccountView) -> ProgramResult {
66    let rent = crate::sysvar::get_rent()?;
67    let min = rent.minimum_balance(account.data_len());
68    if account.lamports() >= min {
69        Ok(())
70    } else {
71        Err(ProgramError::AccountNotRentExempt)
72    }
73}
74
75/// Assert that two accounts have the same address.
76///
77/// Useful for verifying expected accounts match (e.g., token mint
78/// matches the vault's expected mint).
79#[inline]
80pub fn require_same_address(a: &AccountView, b: &AccountView) -> ProgramResult {
81    if crate::address::address_eq(a.address(), b.address()) {
82        Ok(())
83    } else {
84        Err(ProgramError::InvalidArgument)
85    }
86}
87
88/// Assert that an account's address matches an expected address.
89#[inline]
90pub fn require_address(account: &AccountView, expected: &Address) -> ProgramResult {
91    if crate::address::address_eq(account.address(), expected) {
92        Ok(())
93    } else {
94        Err(ProgramError::InvalidArgument)
95    }
96}
97
98/// Assert that an account has the expected discriminator AND is owned
99/// by the given program. This two-check combo is the most common
100/// "is this the right account type?" pattern in Solana programs.
101#[inline]
102pub fn require_account_type(
103    account: &AccountView,
104    expected_disc: u8,
105    expected_owner: &Address,
106) -> ProgramResult {
107    if account.disc() != expected_disc {
108        return Err(ProgramError::InvalidAccountData);
109    }
110    account.require_owned_by(expected_owner)
111}
112
113/// Zero the data bytes of an account without changing lamports or owner.
114///
115/// Useful for "soft close" patterns where you want to mark an account
116/// as consumed but leave it allocated for potential reuse.
117#[inline]
118pub fn zero_data(account: &AccountView) -> ProgramResult {
119    let len = account.data_len();
120    if len == 0 {
121        return Ok(());
122    }
123    let data_ptr = account.data_ptr_unchecked();
124    unsafe {
125        core::ptr::write_bytes(data_ptr, 0, len);
126    }
127    Ok(())
128}
129
130/// Checked realloc that also ensures the account remains rent-exempt
131/// after resizing.
132///
133/// This is the safe version of `account.resize()` -- it verifies that
134/// the account has enough lamports to cover rent at the new data length.
135#[inline]
136pub fn realloc_checked(
137    account: &AccountView,
138    new_len: usize,
139    payer: Option<&AccountView>,
140) -> ProgramResult {
141    // Check rent requirement BEFORE resizing to avoid leaving the account
142    // in an inconsistent state if the payer transfer fails.
143    let rent = crate::sysvar::get_rent()?;
144    let min = rent.minimum_balance(new_len);
145    let current = account.lamports();
146
147    if current < min {
148        // Need more lamports. Transfer BEFORE resize so that if the
149        // transfer fails, the account data length is unchanged.
150        if let Some(payer) = payer {
151            let deficit = min - current;
152            transfer_lamports(payer, account, deficit)?;
153        } else {
154            return Err(ProgramError::AccountNotRentExempt);
155        }
156    }
157
158    // Now resize -- the account already has enough lamports.
159    account.resize(new_len)
160}