use crate::account::AccountView;
use crate::address::address_eq;
use crate::error::ProgramError;
use crate::ProgramResult;
#[inline]
pub fn transfer_lamports(
from: &AccountView<'_>,
to: &AccountView<'_>,
amount: u64,
) -> ProgramResult {
crate::write_policy::check_lamport_mutation(from.address())?;
crate::write_policy::check_lamport_mutation(to.address())?;
from.require_writable()?;
to.require_writable()?;
if address_eq(from.address(), to.address()) {
if from.lamports() < amount {
return Err(ProgramError::InsufficientFunds);
}
return Ok(());
}
let debited = from
.lamports()
.checked_sub(amount)
.ok_or(ProgramError::InsufficientFunds)?;
let credited = to
.lamports()
.checked_add(amount)
.ok_or(ProgramError::ArithmeticOverflow)?;
from.try_set_lamports(debited)?;
to.try_set_lamports(credited)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::write_policy::{install_lamport_gate, write_policy_violation, WritePolicy};
use hopper_native::{
AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount, NOT_BORROWED,
};
fn make_backend(seed: u8, lamports: u64) -> (std::vec::Vec<u64>, NativeAccountView<'static>) {
let mut backing = std::vec![0u64; (RuntimeAccount::SIZE + 32).div_ceil(8)];
let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
unsafe {
raw.write(RuntimeAccount {
borrow_state: NOT_BORROWED,
is_signer: 1,
is_writable: 1,
executable: 0,
resize_delta: 0,
address: NativeAddress::new_from_array([seed; 32]),
owner: NativeAddress::new_from_array([2; 32]),
lamports,
data_len: 32,
});
}
let backend = unsafe { NativeAccountView::new_unchecked(raw) };
(backing, backend)
}
fn make_account(seed: u8, lamports: u64) -> (std::vec::Vec<u64>, AccountView<'static>) {
let (backing, backend) = make_backend(seed, lamports);
(backing, AccountView::from_backend(backend))
}
#[test]
fn ungated_behavior_matches_substrate_helper_exactly() {
let cases: [(u64, u64, u64); 6] = [
(100, 50, 30), (100, 50, 100), (100, 50, 0), (100, 50, 150), (100, u64::MAX, 1), (100, u64::MAX, 150), ];
for (i, &(from_bal, to_bal, amount)) in cases.iter().enumerate() {
let seed = (10 + 4 * i) as u8;
let (_rf, runtime_from) = make_account(seed, from_bal);
let (_rt, runtime_to) = make_account(seed + 1, to_bal);
let (_nf, native_from) = make_backend(seed + 2, from_bal);
let (_nt, native_to) = make_backend(seed + 3, to_bal);
let ours = transfer_lamports(&runtime_from, &runtime_to, amount);
let theirs = hopper_native::batch::transfer_lamports(&native_from, &native_to, amount)
.map_err(ProgramError::from);
assert_eq!(ours, theirs, "case {i}: result diverged");
assert_eq!(
runtime_from.lamports(),
native_from.lamports(),
"case {i}: from balance diverged"
);
assert_eq!(
runtime_to.lamports(),
native_to.lamports(),
"case {i}: to balance diverged"
);
if ours.is_err() {
assert_eq!(runtime_from.lamports(), from_bal, "case {i}");
assert_eq!(runtime_to.lamports(), to_bal, "case {i}");
}
}
}
#[test]
fn gated_transfer_between_declared_accounts_moves_exact_balances() {
let (_b0, from) = make_account(40, 1_000);
let (_b1, to) = make_account(41, 250);
let accounts = [from, to];
static P: WritePolicy = WritePolicy::with_lamports(&[], &[0, 1]);
let _gate = install_lamport_gate(&accounts, &P);
transfer_lamports(&accounts[0], &accounts[1], 400).unwrap();
assert_eq!(accounts[0].lamports(), 600);
assert_eq!(accounts[1].lamports(), 650);
}
#[test]
fn gated_transfer_from_undeclared_account_is_refused_before_any_mutation() {
let (_b0, from) = make_account(42, 1_000);
let (_b1, to) = make_account(43, 250);
let accounts = [from, to];
static P: WritePolicy = WritePolicy::with_lamports(&[], &[1]);
let _gate = install_lamport_gate(&accounts, &P);
assert_eq!(
transfer_lamports(&accounts[0], &accounts[1], 400),
Err(write_policy_violation(0))
);
assert_eq!(accounts[0].lamports(), 1_000);
assert_eq!(accounts[1].lamports(), 250);
}
#[test]
fn gated_transfer_to_undeclared_account_is_refused_before_any_mutation() {
let (_b0, from) = make_account(44, 1_000);
let (_b1, to) = make_account(45, 250);
let accounts = [from, to];
static P: WritePolicy = WritePolicy::with_lamports(&[], &[0]);
let _gate = install_lamport_gate(&accounts, &P);
assert_eq!(
transfer_lamports(&accounts[0], &accounts[1], 400),
Err(write_policy_violation(1))
);
assert_eq!(accounts[0].lamports(), 1_000);
assert_eq!(accounts[1].lamports(), 250);
}
#[test]
fn gated_arithmetic_refusals_keep_indexed_gate_errors_out_of_the_way() {
let (_b0, from) = make_account(46, 100);
let (_b1, to) = make_account(47, u64::MAX);
let accounts = [from, to];
static P: WritePolicy = WritePolicy::with_lamports(&[], &[0, 1]);
let _gate = install_lamport_gate(&accounts, &P);
assert_eq!(
transfer_lamports(&accounts[0], &accounts[1], 150),
Err(ProgramError::InsufficientFunds)
);
assert_eq!(
transfer_lamports(&accounts[0], &accounts[1], 1),
Err(ProgramError::ArithmeticOverflow)
);
assert_eq!(accounts[0].lamports(), 100);
assert_eq!(accounts[1].lamports(), u64::MAX);
}
#[test]
fn ungated_self_transfer_is_balance_checked_net_zero() {
let (_b, a) = make_account(50, 500);
let alias = a.clone();
transfer_lamports(&a, &alias, 200).unwrap();
assert_eq!(a.lamports(), 500);
assert_eq!(
transfer_lamports(&a, &alias, 501),
Err(ProgramError::InsufficientFunds)
);
assert_eq!(a.lamports(), 500);
}
#[test]
fn gated_self_transfer_follows_the_declared_set() {
let (_b0, declared) = make_account(51, 500);
let (_b1, foreign) = make_account(52, 500);
let accounts = [declared];
static P: WritePolicy = WritePolicy::with_lamports(&[], &[0]);
let _gate = install_lamport_gate(&accounts, &P);
let alias = accounts[0].clone();
transfer_lamports(&accounts[0], &alias, 200).unwrap();
assert_eq!(accounts[0].lamports(), 500);
assert_eq!(
transfer_lamports(&accounts[0], &alias, 501),
Err(ProgramError::InsufficientFunds)
);
let foreign_alias = foreign.clone();
assert_eq!(
transfer_lamports(&foreign, &foreign_alias, 1),
Err(write_policy_violation(u8::MAX))
);
assert_eq!(foreign.lamports(), 500);
}
#[test]
fn dropping_the_gate_restores_ungated_passthrough() {
let (_b0, from) = make_account(53, 1_000);
let (_b1, to) = make_account(54, 0);
let accounts = [from, to];
static P: WritePolicy = WritePolicy::with_lamports(&[], &[]);
{
let _gate = install_lamport_gate(&accounts, &P);
assert_eq!(
transfer_lamports(&accounts[0], &accounts[1], 1),
Err(write_policy_violation(0))
);
}
transfer_lamports(&accounts[0], &accounts[1], 1).unwrap();
assert_eq!(accounts[0].lamports(), 999);
assert_eq!(accounts[1].lamports(), 1);
}
#[test]
#[cfg(not(feature = "unguarded-raw-surfaces"))]
fn gated_transfer_composes_with_data_ranges() {
let (_b0, from) = make_account(55, 10);
let (_b1, to) = make_account(56, 10);
let accounts = [from, to];
static P: WritePolicy = WritePolicy::with_lamports(
&[crate::write_policy::WriteRange::whole_account(0)],
&[0, 1],
);
let _gate = install_lamport_gate(&accounts, &P);
transfer_lamports(&accounts[0], &accounts[1], 10).unwrap();
assert_eq!(accounts[0].lamports(), 0);
assert_eq!(accounts[1].lamports(), 20);
}
}