use anchor_lang::prelude::*;
use solana_program::{
account_info::AccountInfo,
clock::Clock,
program::{invoke, invoke_signed},
program_memory::sol_memcmp,
program_pack::{IsInitialized, Pack},
pubkey::{Pubkey, PUBKEY_BYTES},
system_instruction,
};
use spl_associated_token_account::get_associated_token_address;
use crate::{constants::*, CandyError, CandyMachine};
pub fn assert_initialized<T: Pack + IsInitialized>(account_info: &AccountInfo) -> Result<T> {
let account: T = T::unpack_unchecked(&account_info.data.borrow())?;
if !account.is_initialized() {
Err(CandyError::Uninitialized.into())
} else {
Ok(account)
}
}
pub fn cmp_pubkeys(a: &Pubkey, b: &Pubkey) -> bool {
sol_memcmp(a.as_ref(), b.as_ref(), PUBKEY_BYTES) == 0
}
pub fn assert_valid_go_live<'info>(
payer: &Signer<'info>,
clock: &Clock,
candy_machine: &Account<'info, CandyMachine>,
) -> Result<()> {
match candy_machine.data.go_live_date {
None => {
if !cmp_pubkeys(payer.key, &candy_machine.authority) {
return Err(CandyError::CandyMachineNotLive.into());
}
}
Some(val) => {
if clock.unix_timestamp < val && !cmp_pubkeys(payer.key, &candy_machine.authority) {
return Err(CandyError::CandyMachineNotLive.into());
}
}
}
Ok(())
}
pub fn assert_owned_by(account: &AccountInfo, owner: &Pubkey) -> Result<()> {
if !cmp_pubkeys(account.owner, owner) {
Err(CandyError::IncorrectOwner.into())
} else {
Ok(())
}
}
pub struct TokenTransferParams<'a: 'b, 'b> {
pub source: AccountInfo<'a>,
pub destination: AccountInfo<'a>,
pub amount: u64,
pub authority: AccountInfo<'a>,
pub authority_signer_seeds: &'b [&'b [u8]],
pub token_program: AccountInfo<'a>,
}
#[inline(always)]
pub fn spl_token_transfer(params: TokenTransferParams<'_, '_>) -> Result<()> {
let TokenTransferParams {
source,
destination,
authority,
token_program,
amount,
authority_signer_seeds,
} = params;
let mut signer_seeds = vec![];
if !authority_signer_seeds.is_empty() {
signer_seeds.push(authority_signer_seeds)
}
let result = invoke_signed(
&spl_token::instruction::transfer(
token_program.key,
source.key,
destination.key,
authority.key,
&[],
amount,
)?,
&[source, destination, authority, token_program],
&signer_seeds,
);
result.map_err(|_| CandyError::TokenTransferFailed.into())
}
pub fn assert_is_ata(
ata: &AccountInfo,
wallet: &Pubkey,
mint: &Pubkey,
) -> core::result::Result<spl_token::state::Account, ProgramError> {
assert_owned_by(ata, &spl_token::id())?;
let ata_account: spl_token::state::Account = assert_initialized(ata)?;
assert_keys_equal(&ata_account.owner, wallet)?;
assert_keys_equal(&ata_account.mint, mint)?;
assert_keys_equal(&get_associated_token_address(wallet, mint), ata.key)?;
Ok(ata_account)
}
pub fn assert_keys_equal(key1: &Pubkey, key2: &Pubkey) -> Result<()> {
if !cmp_pubkeys(key1, key2) {
err!(CandyError::PublicKeyMismatch)
} else {
Ok(())
}
}
pub struct TokenBurnParams<'a: 'b, 'b> {
pub mint: AccountInfo<'a>,
pub source: AccountInfo<'a>,
pub amount: u64,
pub authority: AccountInfo<'a>,
pub authority_signer_seeds: Option<&'b [&'b [u8]]>,
pub token_program: AccountInfo<'a>,
}
pub fn spl_token_burn(params: TokenBurnParams<'_, '_>) -> Result<()> {
let TokenBurnParams {
mint,
source,
authority,
token_program,
amount,
authority_signer_seeds,
} = params;
let mut seeds: Vec<&[&[u8]]> = vec![];
if let Some(seed) = authority_signer_seeds {
seeds.push(seed);
}
let result = invoke_signed(
&spl_token::instruction::burn(
token_program.key,
source.key,
mint.key,
authority.key,
&[],
amount,
)?,
&[source, mint, authority, token_program],
seeds.as_slice(),
);
result.map_err(|_| CandyError::TokenBurnFailed.into())
}
pub fn is_feature_active(uuid: &str, feature_index: usize) -> bool {
let uuid_bytes = uuid.as_bytes();
if feature_index == COLLECTIONS_FEATURE_INDEX && uuid_bytes[feature_index] == b'1' {
is_valid_uuid(uuid)
} else {
uuid_bytes[feature_index] == b'#'
}
}
fn is_valid_uuid(uuid: &str) -> bool {
!uuid.bytes().any(|b| b != b'1' && b != b'0' && b != b'#')
}
pub fn set_feature_flag(uuid: &mut str, feature_index: usize) {
if feature_index > 5 {
return;
}
unsafe {
uuid.as_bytes_mut()[feature_index] = b'#';
}
}
pub fn remove_feature_flag(uuid: &mut str, feature_index: usize) {
if feature_index > 5 {
return;
}
unsafe {
uuid.as_bytes_mut()[feature_index] = b'0';
}
}
pub fn punish_bots<'a>(
error: CandyError,
bot_account: AccountInfo<'a>,
payment_account: AccountInfo<'a>,
system_program: AccountInfo<'a>,
fee: u64,
) -> Result<()> {
msg!(
"{}, Candy Machine Botting is taxed at {:?} lamports",
error.to_string(),
fee
);
let final_fee = fee.min(bot_account.lamports());
invoke(
&system_instruction::transfer(bot_account.key, payment_account.key, final_fee),
&[bot_account, payment_account, system_program],
)?;
Ok(())
}
#[cfg(test)]
pub mod tests {
use std::{assert_eq, println};
use crate::constants::COLLECTIONS_FEATURE_INDEX;
use super::*;
#[test]
fn feature_flag_working() {
let mut uuid = String::from("ABCDEF");
println!(
"Should be 65: {}",
uuid.as_bytes()[COLLECTIONS_FEATURE_INDEX]
);
remove_feature_flag(&mut uuid, COLLECTIONS_FEATURE_INDEX);
assert_eq!(uuid, "0BCDEF");
uuid = String::from("01H333");
assert!(!is_feature_active(&uuid, FREEZE_FEATURE_INDEX));
assert_eq!(uuid, "01H333");
set_feature_flag(&mut uuid, FREEZE_FEATURE_INDEX);
assert_eq!(uuid, "0#H333");
assert!(is_feature_active(&uuid, FREEZE_FEATURE_INDEX));
remove_feature_flag(&mut uuid, FREEZE_FEATURE_INDEX);
assert!(!is_feature_active(&uuid, FREEZE_FEATURE_INDEX));
println!("Should be 00H333: {}", uuid);
set_feature_flag(&mut uuid, FREEZE_LOCK_FEATURE_INDEX);
assert!(is_feature_active(&uuid, FREEZE_LOCK_FEATURE_INDEX));
assert_eq!(uuid, "00#333");
remove_feature_flag(&mut uuid, COLLECTIONS_FEATURE_INDEX);
assert_eq!(uuid, "00#333");
set_feature_flag(&mut uuid, FREEZE_FEATURE_INDEX);
assert!(is_feature_active(&uuid, FREEZE_FEATURE_INDEX));
assert_eq!(uuid, "0##333");
set_feature_flag(&mut uuid, COLLECTIONS_FEATURE_INDEX);
assert!(is_feature_active(&uuid, COLLECTIONS_FEATURE_INDEX));
assert_eq!(uuid, "###333");
uuid = String::from("1ABCDE");
assert!(!is_feature_active(&uuid, COLLECTIONS_FEATURE_INDEX));
uuid = String::from("100000");
assert!(is_feature_active(&uuid, COLLECTIONS_FEATURE_INDEX));
uuid = String::from("1##000");
assert!(is_feature_active(&uuid, COLLECTIONS_FEATURE_INDEX));
}
#[test]
fn check_keys_equal() {
let key1 = Pubkey::new_unique();
assert!(cmp_pubkeys(&key1, &key1));
}
#[test]
fn check_keys_not_equal() {
let key1 = Pubkey::new_unique();
let key2 = Pubkey::new_unique();
assert!(!cmp_pubkeys(&key1, &key2));
}
}