use crate::constants::{SPL_TOKEN_MINT_SIZE, SYSTEM_PROGRAM_ID};
use crate::error::SolanaKiteError;
use crate::transaction::send_transaction_from_instructions;
use litesvm::LiteSVM;
use solana_instruction::account_meta::AccountMeta;
use solana_instruction::Instruction;
use solana_keypair::Keypair;
use solana_pubkey::Pubkey;
use solana_signer::Signer;
pub const TOKEN_EXTENSIONS_PROGRAM_ID: Pubkey =
solana_pubkey::pubkey!("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb");
const ASSOCIATED_TOKEN_PROGRAM_ID: Pubkey =
solana_pubkey::pubkey!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL");
const TOKEN_EXTENSIONS_BASE_SIZE: usize = 165;
const ACCOUNT_TYPE_SIZE: usize = 1;
const TLV_HEADER_SIZE: usize = 4;
const INSTRUCTION_INITIALIZE_MINT2: u8 = 20;
const INSTRUCTION_MINT_TO: u8 = 7;
const INSTRUCTION_TRANSFER_CHECKED: u8 = 12;
const INSTRUCTION_INITIALIZE_NON_TRANSFERABLE_MINT: u8 = 32;
const INSTRUCTION_TRANSFER_HOOK: u8 = 36;
const INSTRUCTION_TRANSFER_FEE: u8 = 26;
const INSTRUCTION_MINT_CLOSE_AUTHORITY: u8 = 25;
const INSTRUCTION_PERMANENT_DELEGATE: u8 = 35;
const INSTRUCTION_DEFAULT_ACCOUNT_STATE: u8 = 28;
const INSTRUCTION_INTEREST_BEARING_MINT: u8 = 33;
const INSTRUCTION_METADATA_POINTER: u8 = 39;
#[derive(Debug, Clone, Copy)]
pub enum TokenAccountState {
Uninitialized = 0,
Initialized = 1,
Frozen = 2,
}
#[derive(Debug, Clone)]
pub enum MintExtension {
TransferHook {
program_id: Pubkey,
},
TransferFee {
fee_basis_points: u16,
maximum_fee: u64,
},
MintCloseAuthority {
close_authority: Pubkey,
},
PermanentDelegate {
delegate: Pubkey,
},
NonTransferable,
DefaultAccountState {
initial_state: TokenAccountState,
},
InterestBearing {
rate_authority: Pubkey,
rate: i16,
},
MetadataPointer {
authority: Pubkey,
metadata_address: Pubkey,
},
}
impl MintExtension {
fn data_size(&self) -> usize {
match self {
MintExtension::TransferHook { .. } => 64,
MintExtension::TransferFee { .. } => 108,
MintExtension::MintCloseAuthority { .. } => 32,
MintExtension::PermanentDelegate { .. } => 32,
MintExtension::NonTransferable => 0,
MintExtension::DefaultAccountState { .. } => 1,
MintExtension::InterestBearing { .. } => 52,
MintExtension::MetadataPointer { .. } => 64,
}
}
fn tlv_size(&self) -> usize {
TLV_HEADER_SIZE + self.data_size()
}
fn build_init_instruction(&self, mint: &Pubkey, mint_authority: &Pubkey) -> Instruction {
match self {
MintExtension::TransferHook { program_id } => {
let mut data = Vec::with_capacity(2 + 64);
data.push(INSTRUCTION_TRANSFER_HOOK);
data.push(0); data.extend_from_slice(&mint_authority.to_bytes()); data.extend_from_slice(&program_id.to_bytes()); Instruction {
program_id: TOKEN_EXTENSIONS_PROGRAM_ID,
accounts: vec![AccountMeta::new(*mint, false)],
data,
}
}
MintExtension::TransferFee {
fee_basis_points,
maximum_fee,
} => {
let mut data = Vec::with_capacity(2 + 1 + 32 + 1 + 32 + 2 + 8);
data.push(INSTRUCTION_TRANSFER_FEE);
data.push(0); data.push(1);
data.extend_from_slice(&mint_authority.to_bytes());
data.push(1);
data.extend_from_slice(&mint_authority.to_bytes());
data.extend_from_slice(&fee_basis_points.to_le_bytes());
data.extend_from_slice(&maximum_fee.to_le_bytes());
Instruction {
program_id: TOKEN_EXTENSIONS_PROGRAM_ID,
accounts: vec![AccountMeta::new(*mint, false)],
data,
}
}
MintExtension::MintCloseAuthority { close_authority } => {
let mut data = Vec::with_capacity(1 + 1 + 32);
data.push(INSTRUCTION_MINT_CLOSE_AUTHORITY);
data.push(1); data.extend_from_slice(&close_authority.to_bytes());
Instruction {
program_id: TOKEN_EXTENSIONS_PROGRAM_ID,
accounts: vec![AccountMeta::new(*mint, false)],
data,
}
}
MintExtension::PermanentDelegate { delegate } => {
let mut data = Vec::with_capacity(1 + 32);
data.push(INSTRUCTION_PERMANENT_DELEGATE);
data.extend_from_slice(&delegate.to_bytes());
Instruction {
program_id: TOKEN_EXTENSIONS_PROGRAM_ID,
accounts: vec![AccountMeta::new(*mint, false)],
data,
}
}
MintExtension::NonTransferable => Instruction {
program_id: TOKEN_EXTENSIONS_PROGRAM_ID,
accounts: vec![AccountMeta::new(*mint, false)],
data: vec![INSTRUCTION_INITIALIZE_NON_TRANSFERABLE_MINT],
},
MintExtension::DefaultAccountState { initial_state } => {
Instruction {
program_id: TOKEN_EXTENSIONS_PROGRAM_ID,
accounts: vec![AccountMeta::new(*mint, false)],
data: vec![INSTRUCTION_DEFAULT_ACCOUNT_STATE, 0, *initial_state as u8],
}
}
MintExtension::InterestBearing {
rate_authority,
rate,
} => {
let mut data = Vec::with_capacity(2 + 32 + 2);
data.push(INSTRUCTION_INTEREST_BEARING_MINT);
data.push(0); data.extend_from_slice(&rate_authority.to_bytes()); data.extend_from_slice(&rate.to_le_bytes());
Instruction {
program_id: TOKEN_EXTENSIONS_PROGRAM_ID,
accounts: vec![AccountMeta::new(*mint, false)],
data,
}
}
MintExtension::MetadataPointer {
authority,
metadata_address,
} => {
let mut data = Vec::with_capacity(2 + 64);
data.push(INSTRUCTION_METADATA_POINTER);
data.push(0); data.extend_from_slice(&authority.to_bytes());
data.extend_from_slice(&metadata_address.to_bytes());
Instruction {
program_id: TOKEN_EXTENSIONS_PROGRAM_ID,
accounts: vec![AccountMeta::new(*mint, false)],
data,
}
}
}
}
}
fn calculate_mint_size(extensions: &[MintExtension]) -> usize {
if extensions.is_empty() {
return SPL_TOKEN_MINT_SIZE;
}
let extension_data_size: usize = extensions.iter().map(|ext| ext.tlv_size()).sum();
TOKEN_EXTENSIONS_BASE_SIZE + ACCOUNT_TYPE_SIZE + extension_data_size
}
pub fn create_token_extensions_mint(
litesvm: &mut LiteSVM,
mint_authority: &Keypair,
decimals: u8,
mint: Option<Pubkey>,
extensions: &[MintExtension],
) -> Result<Pubkey, SolanaKiteError> {
let mint = mint.unwrap_or(Pubkey::new_unique());
let mint_size = calculate_mint_size(extensions);
let rent = litesvm.minimum_balance_for_rent_exemption(mint_size);
litesvm
.set_account(
mint,
solana_account::Account {
lamports: rent,
data: vec![0u8; mint_size],
owner: TOKEN_EXTENSIONS_PROGRAM_ID,
executable: false,
rent_epoch: 0,
},
)
.map_err(|e| {
SolanaKiteError::TokenOperationFailed(format!(
"Failed to create Token Extensions mint account: {}",
e
))
})?;
let mut instructions: Vec<Instruction> = extensions
.iter()
.map(|ext| ext.build_init_instruction(&mint, &mint_authority.pubkey()))
.collect();
let mut init_mint_data = Vec::with_capacity(1 + 1 + 32 + 1);
init_mint_data.push(INSTRUCTION_INITIALIZE_MINT2);
init_mint_data.push(decimals);
init_mint_data.extend_from_slice(&mint_authority.pubkey().to_bytes());
init_mint_data.push(0);
instructions.push(Instruction {
program_id: TOKEN_EXTENSIONS_PROGRAM_ID,
accounts: vec![AccountMeta::new(mint, false)],
data: init_mint_data,
});
send_transaction_from_instructions(
litesvm,
instructions,
&[mint_authority],
&mint_authority.pubkey(),
)?;
Ok(mint)
}
pub fn create_token_extensions_account(
litesvm: &mut LiteSVM,
owner: &Pubkey,
mint: &Pubkey,
payer: &Keypair,
) -> Result<Pubkey, SolanaKiteError> {
let associated_token_account = get_token_extensions_account_address(owner, mint);
let instruction = Instruction {
program_id: ASSOCIATED_TOKEN_PROGRAM_ID,
accounts: vec![
AccountMeta::new(payer.pubkey(), true), AccountMeta::new(associated_token_account, false), AccountMeta::new_readonly(*owner, false), AccountMeta::new_readonly(*mint, false), AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false), AccountMeta::new_readonly(TOKEN_EXTENSIONS_PROGRAM_ID, false), ],
data: Vec::new(), };
send_transaction_from_instructions(litesvm, vec![instruction], &[payer], &payer.pubkey())?;
Ok(associated_token_account)
}
pub fn mint_tokens_to_token_extensions_account(
litesvm: &mut LiteSVM,
mint: &Pubkey,
token_account: &Pubkey,
amount: u64,
mint_authority: &Keypair,
) -> Result<(), SolanaKiteError> {
let mut data = vec![INSTRUCTION_MINT_TO];
data.extend_from_slice(&amount.to_le_bytes());
let instruction = Instruction {
program_id: TOKEN_EXTENSIONS_PROGRAM_ID,
accounts: vec![
AccountMeta::new(*mint, false),
AccountMeta::new(*token_account, false),
AccountMeta::new_readonly(mint_authority.pubkey(), true),
],
data,
};
send_transaction_from_instructions(
litesvm,
vec![instruction],
&[mint_authority],
&mint_authority.pubkey(),
)?;
Ok(())
}
#[allow(clippy::too_many_arguments)] pub fn transfer_checked_token_extensions(
litesvm: &mut LiteSVM,
source: &Pubkey,
mint: &Pubkey,
destination: &Pubkey,
authority: &Keypair,
amount: u64,
decimals: u8,
hook_accounts: &[AccountMeta],
) -> Result<(), SolanaKiteError> {
let mut data = vec![INSTRUCTION_TRANSFER_CHECKED];
data.extend_from_slice(&amount.to_le_bytes());
data.push(decimals);
let mut accounts = vec![
AccountMeta::new(*source, false),
AccountMeta::new_readonly(*mint, false),
AccountMeta::new(*destination, false),
AccountMeta::new_readonly(authority.pubkey(), true),
];
accounts.extend_from_slice(hook_accounts);
let instruction = Instruction {
program_id: TOKEN_EXTENSIONS_PROGRAM_ID,
accounts,
data,
};
send_transaction_from_instructions(
litesvm,
vec![instruction],
&[authority],
&authority.pubkey(),
)?;
Ok(())
}
#[must_use]
pub fn get_token_extensions_account_address(owner: &Pubkey, mint: &Pubkey) -> Pubkey {
spl_associated_token_account::get_associated_token_address_with_program_id(
owner,
mint,
&TOKEN_EXTENSIONS_PROGRAM_ID,
)
}