Skip to content
This repository was archived by the owner on Feb 13, 2025. It is now read-only.

feature: Added batch instruction #7

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion interface/src/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use pinocchio::{program_error::ProgramError, pubkey::Pubkey};
use crate::error::TokenError;

/// Instructions supported by the token program.
#[repr(C)]
#[repr(C, u8)]
#[derive(Clone, Debug, PartialEq)]
pub enum TokenInstruction<'a> {
/// Initializes a new mint and optionally deposits all the newly minted
Expand Down Expand Up @@ -477,6 +477,21 @@ pub enum TokenInstruction<'a> {
/// The ui_amount of tokens to reformat.
ui_amount: &'a str,
},

/// Executes a batch of instructions. The instructions to be executed are specified
/// in sequence on the instruction data. Each instruction provides:
/// - `u8`: number of accounts
/// - `u8`: instruction data length (includes the discriminator)
/// - `u8`: instruction discriminator
/// - `[u8]`: instruction data
///
/// Accounts follow a similar pattern, where accounts for each instruction are
/// specified in sequence. Therefore, the number of accounts expected by this
/// instruction is variable – i.e., it depends on the instructions provided.
///
/// Both the number of accountsa and instruction data length are used to identify
/// the slice of accounts and instruction data for each instruction.
Batch = 255,
// Any new variants also need to be added to program-2022 `TokenInstruction`, so that the
// latter remains a superset of this instruction set. New variants also need to be added to
// token/js/src/instructions/types.ts to maintain @solana/spl-token compatibility
Expand Down
6 changes: 3 additions & 3 deletions interface/src/state/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ pub struct Account {
delegate: COption<Pubkey>,

/// The account's state.
pub state: AccountState,
pub state: u8,

/// Indicates whether this account represents a native token or not.
is_native: [u8; 4],
Expand Down Expand Up @@ -131,7 +131,7 @@ impl Account {

#[inline(always)]
pub fn is_frozen(&self) -> bool {
self.state == AccountState::Frozen
self.state == AccountState::Frozen as u8
}

#[inline(always)]
Expand All @@ -147,6 +147,6 @@ impl RawType for Account {
impl Initializable for Account {
#[inline(always)]
fn is_initialized(&self) -> bool {
self.state != AccountState::Uninitialized
self.state != AccountState::Uninitialized as u8
}
}
72 changes: 45 additions & 27 deletions program/src/entrypoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,27 @@ no_allocator!();
// Use the default panic handler.
default_panic_handler!();

#[inline(always)]
pub fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let [discriminator, instruction_data @ ..] = instruction_data else {
return Err(ProgramError::InvalidInstructionData);
};

if *discriminator == 255 {
// 255 - Batch
#[cfg(feature = "logging")]
pinocchio::msg!("Instruction: Batch");

return process_batch(accounts, instruction_data);
}

inner_process_instruction(accounts, instruction_data, *discriminator)
}

/// Process an instruction.
///
/// The processor of the token program is divided into two parts to reduce the overhead
Expand All @@ -21,31 +42,35 @@ default_panic_handler!();
///
/// Instructions on the first part of the processor:
///
/// - `0`: `InitializeMint`
/// - `3`: `Transfer`
/// - `7`: `MintTo`
/// - `9`: `CloseAccount`
/// - `0`: `InitializeMint`
/// - `1`: `InitializeAccount`
/// - `3`: `Transfer`
/// - `7`: `MintTo`
/// - `9`: `CloseAccount`
/// - `18`: `InitializeAccount2`
/// - `18`: `InitializeAccount3`
/// - `20`: `InitializeMint2`
#[inline(always)]
pub fn process_instruction(
_program_id: &Pubkey,
pub fn inner_process_instruction(
accounts: &[AccountInfo],
instruction_data: &[u8],
discriminator: u8,
) -> ProgramResult {
let (discriminator, instruction_data) = instruction_data
.split_first()
.ok_or(ProgramError::InvalidInstructionData)?;

match *discriminator {
match discriminator {
// 0 - InitializeMint
0 => {
#[cfg(feature = "logging")]
pinocchio::msg!("Instruction: InitializeMint");

process_initialize_mint(accounts, instruction_data, true)
}
// 1 - InitializeAccount
1 => {
#[cfg(feature = "logging")]
pinocchio::msg!("Instruction: InitializeAccount");

process_initialize_account(accounts)
}
// 3 - Transfer
3 => {
#[cfg(feature = "logging")]
Expand All @@ -67,6 +92,13 @@ pub fn process_instruction(

process_close_account(accounts)
}
// 16 - InitializeAccount2
16 => {
#[cfg(feature = "logging")]
pinocchio::msg!("Instruction: InitializeAccount2");

process_initialize_account2(accounts, instruction_data)
}
// 18 - InitializeAccount3
18 => {
#[cfg(feature = "logging")]
Expand All @@ -81,7 +113,7 @@ pub fn process_instruction(

process_initialize_mint2(accounts, instruction_data)
}
_ => process_remaining_instruction(accounts, instruction_data, *discriminator),
_ => inner_process_remaining_instruction(accounts, instruction_data, discriminator),
}
}

Expand All @@ -90,19 +122,12 @@ pub fn process_instruction(
/// This function is called by the `process_instruction` function if the discriminator
/// does not match any of the common instructions. This function is used to reduce the
/// overhead of having a large `match` statement in the `process_instruction` function.
fn process_remaining_instruction(
fn inner_process_remaining_instruction(
accounts: &[AccountInfo],
instruction_data: &[u8],
discriminator: u8,
) -> ProgramResult {
match discriminator {
// 1 - InitializeAccount
1 => {
#[cfg(feature = "logging")]
pinocchio::msg!("Instruction: InitializeAccount");

process_initialize_account(accounts)
}
// 2 - InitializeMultisig
2 => {
#[cfg(feature = "logging")]
Expand Down Expand Up @@ -180,13 +205,6 @@ fn process_remaining_instruction(

process_burn_checked(accounts, instruction_data)
}
// 16 - InitializeAccount2
16 => {
#[cfg(feature = "logging")]
pinocchio::msg!("Instruction: InitializeAccount2");

process_initialize_account2(accounts, instruction_data)
}
// 17 - SyncNative
17 => {
#[cfg(feature = "logging")]
Expand Down
54 changes: 54 additions & 0 deletions program/src/processor/batch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult};

use crate::entrypoint::inner_process_instruction;

/// The size of the batch instruction header.
///
/// The header of each instruction consists of two `u8` values:
/// * number of the accounts
/// * length of the instruction data
const IX_HEADER_SIZE: usize = 2;

pub fn process_batch(mut accounts: &[AccountInfo], mut instruction_data: &[u8]) -> ProgramResult {
loop {
// Validates the instruction data and accounts offset.

if instruction_data.len() < IX_HEADER_SIZE {
// The instruction data must have at least two bytes.
return Err(ProgramError::InvalidInstructionData);
}

// SAFETY: The instruction data is guaranteed to have at least two bytes (header)
// + one byte (discriminator).
let expected_accounts = unsafe { *instruction_data.get_unchecked(0) as usize };
let data_offset = IX_HEADER_SIZE + unsafe { *instruction_data.get_unchecked(1) as usize };

if instruction_data.len() < data_offset || data_offset == 0 {
return Err(ProgramError::InvalidInstructionData);
}

if accounts.len() < expected_accounts {
return Err(ProgramError::NotEnoughAccountKeys);
}

// Process the instruction.

// SAFETY: The instruction data and accounts lengths are already validated so all
// the slices are guaranteed to be valid.
inner_process_instruction(
unsafe { accounts.get_unchecked(..expected_accounts) },
unsafe { instruction_data.get_unchecked(IX_HEADER_SIZE + 1..data_offset) },
unsafe { *instruction_data.get_unchecked(IX_HEADER_SIZE) },
)?;

if data_offset == instruction_data.len() {
// The batch is complete.
break;
}

accounts = &accounts[expected_accounts..];
instruction_data = &instruction_data[data_offset..];
}

Ok(())
}
2 changes: 1 addition & 1 deletion program/src/processor/get_account_data_size.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use super::check_account_owner;

#[inline(always)]
pub fn process_get_account_data_size(accounts: &[AccountInfo]) -> ProgramResult {
let [mint_info, _remaning @ ..] = accounts else {
let [mint_info, _remaining @ ..] = accounts else {
return Err(ProgramError::NotEnoughAccountKeys);
};

Expand Down
2 changes: 2 additions & 0 deletions program/src/processor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use token_interface::{
pub mod amount_to_ui_amount;
pub mod approve;
pub mod approve_checked;
pub mod batch;
pub mod burn;
pub mod burn_checked;
pub mod close_account;
Expand Down Expand Up @@ -49,6 +50,7 @@ pub mod shared;
pub use amount_to_ui_amount::process_amount_to_ui_amount;
pub use approve::process_approve;
pub use approve_checked::process_approve_checked;
pub use batch::process_batch;
pub use burn::process_burn;
pub use burn_checked::process_burn_checked;
pub use close_account::process_close_account;
Expand Down
4 changes: 2 additions & 2 deletions program/src/processor/revoke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use super::validate_owner;

#[inline(always)]
pub fn process_revoke(accounts: &[AccountInfo], _instruction_data: &[u8]) -> ProgramResult {
let [source_account_info, owner_info, remaning @ ..] = accounts else {
let [source_account_info, owner_info, remaining @ ..] = accounts else {
return Err(ProgramError::NotEnoughAccountKeys);
};

Expand All @@ -21,7 +21,7 @@ pub fn process_revoke(accounts: &[AccountInfo], _instruction_data: &[u8]) -> Pro
return Err(TokenError::AccountFrozen.into());
}

validate_owner(&source_account.owner, owner_info, remaning)?;
validate_owner(&source_account.owner, owner_info, remaining)?;

source_account.clear_delegate();
source_account.set_delegated_amount(0);
Expand Down
10 changes: 5 additions & 5 deletions program/src/processor/set_authority.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub fn process_set_authority(accounts: &[AccountInfo], instruction_data: &[u8])

// Validates the accounts.

let [account_info, authority_info, remaning @ ..] = accounts else {
let [account_info, authority_info, remaining @ ..] = accounts else {
return Err(ProgramError::NotEnoughAccountKeys);
};

Expand All @@ -37,7 +37,7 @@ pub fn process_set_authority(accounts: &[AccountInfo], instruction_data: &[u8])

match authority_type {
AuthorityType::AccountOwner => {
validate_owner(&account.owner, authority_info, remaning)?;
validate_owner(&account.owner, authority_info, remaining)?;

if let Some(authority) = new_authority {
account.owner = *authority;
Expand All @@ -54,7 +54,7 @@ pub fn process_set_authority(accounts: &[AccountInfo], instruction_data: &[u8])
}
AuthorityType::CloseAccount => {
let authority = account.close_authority().unwrap_or(&account.owner);
validate_owner(authority, authority_info, remaning)?;
validate_owner(authority, authority_info, remaining)?;

if let Some(authority) = new_authority {
account.set_close_authority(authority);
Expand All @@ -77,7 +77,7 @@ pub fn process_set_authority(accounts: &[AccountInfo], instruction_data: &[u8])
// mint_authority.
let mint_authority = mint.mint_authority().ok_or(TokenError::FixedSupply)?;

validate_owner(mint_authority, authority_info, remaning)?;
validate_owner(mint_authority, authority_info, remaining)?;

if let Some(authority) = new_authority {
mint.set_mint_authority(authority);
Expand All @@ -92,7 +92,7 @@ pub fn process_set_authority(accounts: &[AccountInfo], instruction_data: &[u8])
.freeze_authority()
.ok_or(TokenError::MintCannotFreeze)?;

validate_owner(freeze_authority, authority_info, remaning)?;
validate_owner(freeze_authority, authority_info, remaining)?;

if let Some(authority) = new_authority {
mint.set_freeze_authority(authority);
Expand Down
8 changes: 4 additions & 4 deletions program/src/processor/shared/approve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub fn process_approve(

let (source_account_info, expected_mint_info, delegate_info, owner_info, remaining) =
if let Some(expected_decimals) = expected_decimals {
let [source_account_info, expected_mint_info, delegate_info, owner_info, remaning @ ..] =
let [source_account_info, expected_mint_info, delegate_info, owner_info, remaining @ ..] =
accounts
else {
return Err(ProgramError::NotEnoughAccountKeys);
Expand All @@ -28,18 +28,18 @@ pub fn process_approve(
Some((expected_mint_info, expected_decimals)),
delegate_info,
owner_info,
remaning,
remaining,
)
} else {
let [source_account_info, delegate_info, owner_info, remaning @ ..] = accounts else {
let [source_account_info, delegate_info, owner_info, remaining @ ..] = accounts else {
return Err(ProgramError::NotEnoughAccountKeys);
};
(
source_account_info,
None,
delegate_info,
owner_info,
remaning,
remaining,
)
};

Expand Down
Loading