pina 0.6.0

a solana and pinocchio smart contract framework
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
//! CPI and account-allocation helpers used by on-chain instruction handlers.
//!
//! These utilities wrap common system-program patterns (create, allocate,
//! assign, close) with consistent `ProgramError` behavior and PDA signing.
//! All APIs in this module are designed for on-chain determinism and return
//! `ProgramError` values for caller-side propagation with `?` instead of
//! panicking.
//!
//! Seed-based helpers require deterministic seed ordering and consistent
//! program IDs across derivation and verification.

use bytemuck::Pod;
use pinocchio::AccountView;
use pinocchio::Address;
use pinocchio::cpi::Seed;
use pinocchio::cpi::Signer;
use pinocchio::error::ProgramError;
use pinocchio::sysvars::Sysvar;
use pinocchio::sysvars::rent::Rent;
use pinocchio_system::instructions::Allocate;
use pinocchio_system::instructions::Assign;
use pinocchio_system::instructions::CreateAccount;
use pinocchio_system::instructions::Transfer;

use crate::HasDiscriminator;
use crate::LamportTransfer;
use crate::MAX_SEEDS;
use crate::ProgramResult;

/// Creates a new system account owned by `owner`.
///
/// Calculates the rent-exempt balance for `space`, then issues a single
/// `CreateAccount` CPI from `from` to `to`.
///
/// # Errors
///
/// Returns errors from rent sysvar access, rent minimum-balance computation,
/// or the underlying system-program CPI.
///
/// # Examples
///
/// ```ignore
/// use pina::cpi::create_account;
///
/// // Create a new account with 128 bytes of space owned by `program_id`:
/// create_account(payer, new_account, 128, &program_id)?;
/// ```
#[inline(always)]
pub fn create_account<'a>(
	from: &'a AccountView,
	to: &'a AccountView,
	space: usize,
	owner: &Address,
) -> ProgramResult {
	let lamports = Rent::get()?.try_minimum_balance(space)?;

	CreateAccount {
		from,
		to,
		lamports,
		space: space as u64,
		owner,
	}
	.invoke()
}

/// Creates a new PDA-backed program account and returns `(address, bump)`.
///
/// This helper derives the canonical PDA for `seeds` + `owner`, allocates
/// account storage for `T`, and assigns account ownership to `owner`.
///
/// <!-- {=pinaPdaSeedContract|trim|linePrefix:"/// ":true} -->/// Seed-based APIs require deterministic seed ordering.
///
/// Program IDs must stay consistent across derivation and verification.
///
/// When a bump is required, prefer canonical bump derivation.
///
/// Use explicit bumps when needed.<!-- {/pinaPdaSeedContract} -->
///
/// # Errors
///
/// Returns `InvalidSeeds` when no valid PDA can be derived, plus any errors
/// from allocation/assignment steps.
///
/// # Examples
///
/// ```ignore
/// // Create a PDA-backed escrow account:
/// let seeds: &[&[u8]] = &[b"escrow", authority.address().as_ref()];
/// let (address, bump) =
/// 	create_program_account::<EscrowState>(escrow_account, payer, &program_id, seeds)?;
/// ```
#[inline(always)]
pub fn create_program_account<'a, T: HasDiscriminator + Pod>(
	target_account: &'a AccountView,
	payer: &'a AccountView,
	owner: &Address,
	seeds: &[&[u8]],
) -> Result<(Address, u8), ProgramError> {
	let Some((address, bump)) = crate::try_find_program_address(seeds, owner) else {
		return Err(ProgramError::InvalidSeeds);
	};

	create_program_account_with_bump::<T>(target_account, payer, owner, seeds, bump)?;

	Ok((address, bump))
}

/// Creates a new PDA-backed program account using a caller-provided `bump`.
///
/// Prefer [`create_program_account`] when you want canonical bump derivation.
/// Use this function when the bump is instruction data and must be validated.
///
/// <!-- {=pinaPdaSeedContract|trim|linePrefix:"/// ":true} -->/// Seed-based APIs require deterministic seed ordering.
///
/// Program IDs must stay consistent across derivation and verification.
///
/// When a bump is required, prefer canonical bump derivation.
///
/// Use explicit bumps when needed.<!-- {/pinaPdaSeedContract} -->
///
/// # Errors
///
/// Returns any error produced by [`allocate_account_with_bump`], including
/// invalid seed layouts and system-program CPI failures.
///
/// # Examples
///
/// ```ignore
/// // Create a PDA-backed account when you already know the bump:
/// let seeds: &[&[u8]] = &[b"escrow", authority.address().as_ref()];
/// create_program_account_with_bump::<EscrowState>(
/// 	escrow_account, payer, &program_id, seeds, bump,
/// )?;
/// ```
#[inline(always)]
pub fn create_program_account_with_bump<'a, T: HasDiscriminator + Pod>(
	target_account: &'a AccountView,
	payer: &'a AccountView,
	owner: &Address,
	seeds: &[&[u8]],
	bump: u8,
) -> ProgramResult {
	// Allocate space.
	allocate_account_with_bump(target_account, payer, size_of::<T>(), owner, seeds, bump)?;

	Ok(())
}

/// Allocates space for a new program account, returning the derived `address`
/// and the canonical `bump`.
///
/// This is the lower-level allocator used by [`create_program_account`] for
/// cases where caller code wants manual discriminator/data initialization.
///
/// <!-- {=pinaPdaSeedContract|trim|linePrefix:"/// ":true} -->/// Seed-based APIs require deterministic seed ordering.
///
/// Program IDs must stay consistent across derivation and verification.
///
/// When a bump is required, prefer canonical bump derivation.
///
/// Use explicit bumps when needed.<!-- {/pinaPdaSeedContract} -->
///
/// # Errors
///
/// Returns `InvalidSeeds` when no canonical PDA can be derived, plus any
/// allocation errors surfaced by [`allocate_account_with_bump`].
///
/// # Examples
///
/// ```ignore
/// // Allocate raw space for manual initialization:
/// let seeds: &[&[u8]] = &[b"vault"];
/// let (address, bump) =
/// 	allocate_account(vault_account, payer, 64, &program_id, seeds)?;
/// ```
#[inline(always)]
pub fn allocate_account<'a>(
	target_account: &'a AccountView,
	payer: &'a AccountView,
	space: usize,
	owner: &Address,
	seeds: &[&[u8]],
) -> Result<(Address, u8), ProgramError> {
	let Some((address, bump)) = crate::try_find_program_address(seeds, owner) else {
		return Err(ProgramError::InvalidSeeds);
	};

	allocate_account_with_bump(target_account, payer, space, owner, seeds, bump)?;

	Ok((address, bump))
}

/// Appends a single-byte bump seed to the provided seeds array, returning
/// a fixed-size `[Seed; MAX_SEEDS]` suitable for PDA signing.
///
/// # Errors
///
/// Returns `ProgramError::InvalidSeeds` if `seeds.len() >= MAX_SEEDS`.
///
/// <!-- {=pinaPdaSeedContract|trim|linePrefix:"/// ":true} -->/// Seed-based APIs require deterministic seed ordering.
///
/// Program IDs must stay consistent across derivation and verification.
///
/// When a bump is required, prefer canonical bump derivation.
///
/// Use explicit bumps when needed.<!-- {/pinaPdaSeedContract} -->
///
/// # Examples
///
/// ```ignore
/// let seeds: &[&[u8]] = &[b"escrow", authority.address().as_ref()];
/// let bump_bytes = [bump];
/// let combined = combine_seeds_with_bump(seeds, &bump_bytes)?;
/// let signer = Signer::from(&combined[..=seeds.len()]);
/// ```
pub fn combine_seeds_with_bump<'a>(
	seeds: &[&'a [u8]],
	bump: &'a [u8; 1],
) -> Result<[Seed<'a>; MAX_SEEDS], ProgramError> {
	if seeds.len() >= MAX_SEEDS {
		return Err(ProgramError::InvalidSeeds);
	}

	// Create our backing storage on the stack, initialized with empty seeds.
	let mut storage: [Seed<'a>; MAX_SEEDS] = core::array::from_fn(|_| Seed::from(&[] as &[u8]));

	// 1. Copy the original seeds into our storage array.
	for (i, seed) in seeds.iter().enumerate() {
		storage[i] = Seed::from(*seed);
	}

	// 2. Add the single-byte bump slice to the end.
	let seeds_len = seeds.len();
	storage[seeds_len] = Seed::from(bump.as_slice());

	Ok(storage)
}

/// Allocates space for a new program account with user-provided bump.
///
/// Two paths are taken depending on whether the target account already has
/// lamports:
///
/// - **Zero balance** -- a single `CreateAccount` CPI is issued.
/// - **Non-zero balance** -- a `Transfer` (to top up rent), `Allocate`, and
///   `Assign` are issued separately. This covers the case where the account was
///   pre-funded (e.g. by a previous failed transaction).
///
/// <!-- {=pinaPdaSeedContract|trim|linePrefix:"/// ":true} -->/// Seed-based APIs require deterministic seed ordering.
///
/// Program IDs must stay consistent across derivation and verification.
///
/// When a bump is required, prefer canonical bump derivation.
///
/// Use explicit bumps when needed.<!-- {/pinaPdaSeedContract} -->
///
/// # Errors
///
/// Returns seed-validation errors, rent sysvar access errors, and any
/// system-program CPI failure from `CreateAccount`, `Transfer`, `Allocate`, or
/// `Assign`.
///
/// # Examples
///
/// ```ignore
/// let seeds: &[&[u8]] = &[b"vault"];
/// allocate_account_with_bump(vault_account, payer, 64, &program_id, seeds, bump)?;
/// ```
#[inline(always)]
pub fn allocate_account_with_bump<'a>(
	target_account: &'a AccountView,
	payer: &'a AccountView,
	space: usize,
	owner: &Address,
	seeds: &[&[u8]],
	bump: u8,
) -> ProgramResult {
	// Combine seeds
	let bump_array = [bump];
	let combined_seeds = combine_seeds_with_bump(seeds, &bump_array)?;
	let seeds_slice = &combined_seeds[..=seeds.len()];
	let signer = Signer::from(seeds_slice);
	let signers = &[signer];
	// Allocate space for account
	let rent = Rent::get()?;

	if target_account.lamports().eq(&0) {
		let lamports = rent.try_minimum_balance(space)?;

		CreateAccount {
			from: payer,
			to: target_account,
			lamports,
			space: space as u64,
			owner,
		}
		.invoke_signed(signers)?;
	} else {
		// Otherwise, if balance is nonzero:

		// 1) transfer sufficient lamports for rent exemption
		let rent_exempt_balance = rent
			.try_minimum_balance(space)?
			.saturating_sub(target_account.lamports());
		if rent_exempt_balance > 0 {
			Transfer {
				from: payer,
				to: target_account,
				lamports: rent_exempt_balance,
			}
			.invoke_signed(signers)?;
		}

		// 2) allocate space for the account
		Allocate {
			account: target_account,
			space: space as u64,
		}
		.invoke_signed(signers)?;

		// 3) assign our program as the owner
		Assign {
			account: target_account,
			owner,
		}
		.invoke_signed(signers)?;
	}

	Ok(())
}

/// Maximum number of bytes an account may grow by in a single instruction.
///
/// This limit is enforced by the Solana runtime. Attempting to grow an account
/// by more than this amount will cause `resize` to return
/// `ProgramError::InvalidRealloc`.
pub const MAX_PERMITTED_DATA_INCREASE: usize = 10_240;

/// Reallocates an account to `new_size` bytes, adjusting rent automatically.
///
/// When **growing**, transfers the additional rent-exempt lamports required from
/// `payer` to `account` via a system-program CPI. When **shrinking**, returns
/// excess rent lamports from `account` to `payer` by direct lamport
/// manipulation (the account must be owned by the executing program for this
/// path).
///
/// New bytes are zero-initialized by the Solana runtime.
///
/// # Limits
///
/// The Solana runtime limits account growth to [`MAX_PERMITTED_DATA_INCREASE`]
/// (10 KiB) per top-level instruction. Exceeding this limit returns
/// `ProgramError::InvalidRealloc`.
///
/// # Errors
///
/// Returns `ProgramError::InvalidAccountData` if the account is not writable,
/// `ProgramError::InvalidAccountOwner` if the account is not owned by
/// `program_id`, and propagates any errors from rent sysvar access, lamport
/// transfer, or the runtime `resize` call.
#[inline(always)]
pub fn realloc_account<'a>(
	account: &'a AccountView,
	new_size: usize,
	payer: &'a AccountView,
	program_id: &Address,
) -> ProgramResult {
	realloc_account_inner(account, new_size, payer, program_id)
}

/// Reallocates an account to `new_size` bytes with explicit zero-initialization,
/// adjusting rent automatically.
///
/// This function behaves identically to [`realloc_account`]. In the current
/// Solana runtime, new bytes are always zero-initialized regardless of which
/// variant is called. This function exists for API symmetry with the runtime's
/// `realloc(new_len, zero_init)` parameter and to make zero-initialization
/// intent explicit at the call site.
///
/// When **growing**, transfers the additional rent-exempt lamports required from
/// `payer` to `account` via a system-program CPI. When **shrinking**, returns
/// excess rent lamports from `account` to `payer` by direct lamport
/// manipulation (the account must be owned by the executing program for this
/// path).
///
/// # Limits
///
/// The Solana runtime limits account growth to [`MAX_PERMITTED_DATA_INCREASE`]
/// (10 KiB) per top-level instruction. Exceeding this limit returns
/// `ProgramError::InvalidRealloc`.
///
/// # Errors
///
/// Returns `ProgramError::InvalidAccountData` if the account is not writable,
/// `ProgramError::InvalidAccountOwner` if the account is not owned by
/// `program_id`, and propagates any errors from rent sysvar access, lamport
/// transfer, or the runtime `resize` call.
#[inline(always)]
pub fn realloc_account_zero<'a>(
	account: &'a AccountView,
	new_size: usize,
	payer: &'a AccountView,
	program_id: &Address,
) -> ProgramResult {
	realloc_account_inner(account, new_size, payer, program_id)
}

/// Shared implementation for [`realloc_account`] and [`realloc_account_zero`].
///
/// Validates the account, computes the rent delta, performs the lamport
/// transfer, and resizes the account data.
#[inline(always)]
fn realloc_account_inner<'a>(
	account: &'a AccountView,
	new_size: usize,
	payer: &'a AccountView,
	program_id: &Address,
) -> ProgramResult {
	use crate::AccountInfoValidation;

	// Validate the account is writable and owned by the program.
	account.assert_writable()?.assert_owner(program_id)?;

	let current_size = account.data_len();

	// Early return when the size is unchanged.
	if new_size == current_size {
		return Ok(());
	}

	let rent = Rent::get()?;
	let new_minimum_balance = rent.try_minimum_balance(new_size)?;
	let current_lamports = account.lamports();

	if new_size > current_size {
		// Growing: transfer additional rent from payer to account.
		let required_lamports = new_minimum_balance.saturating_sub(current_lamports);
		if required_lamports > 0 {
			Transfer {
				from: payer,
				to: account,
				lamports: required_lamports,
			}
			.invoke()?;
		}
	} else {
		// Shrinking: return excess rent from account to payer.
		let excess_lamports = current_lamports.saturating_sub(new_minimum_balance);
		if excess_lamports > 0 {
			account.send(excess_lamports, payer)?;
		}
	}

	// Resize the account data. The runtime zero-initializes new bytes.
	account.resize(new_size)
}

/// Closes an account and returns the remaining rent lamports to the provided
/// recipient.
///
/// Zeroes account data before closing to prevent stale data from being read
/// by subsequent transactions.
///
/// <!-- {=pinaPublicResultContract|trim|linePrefix:"/// ":true} -->/// All APIs in this section are designed for on-chain determinism.
///
/// They return `ProgramError` values for caller-side propagation with `?`.
///
/// No panics needed.<!-- {/pinaPublicResultContract} -->
///
/// # Errors
///
/// Returns errors from lamport transfer, data resize, or account close
/// operations.
///
/// # Examples
///
/// ```ignore
/// // Close the escrow account and return rent to the authority:
/// close_account(escrow_account, authority)?;
/// ```
#[inline(always)]
pub fn close_account(account_info: &AccountView, recipient: &AccountView) -> ProgramResult {
	// Return rent lamports.
	account_info.send(account_info.lamports(), recipient)?;
	// Zero account data before closing.
	account_info.resize(0)?;
	// Close the account.
	account_info.close()
}