solana-program-library/token/transfer-hook-example/tests/functional.rs

496 lines
14 KiB
Rust

// Mark this test as SBF-only due to current `ProgramTest` limitations when CPIing into the system program
#![cfg(feature = "test-sbf")]
use {
solana_program_test::{
processor,
tokio::{self, sync::Mutex},
ProgramTest, ProgramTestContext,
},
solana_sdk::{
account_info::AccountInfo,
entrypoint::ProgramResult,
instruction::{AccountMeta, InstructionError},
pubkey::Pubkey,
signature::Signer,
signer::keypair::Keypair,
system_instruction, sysvar,
transaction::{Transaction, TransactionError},
},
spl_tlv_account_resolution::state::ExtraAccountMetas,
spl_token_client::{
client::{
ProgramBanksClient, ProgramBanksClientProcessTransaction, ProgramClient,
SendTransaction,
},
token::Token,
},
spl_transfer_hook_interface::{
error::TransferHookError,
get_extra_account_metas_address,
instruction::{execute_with_extra_account_metas, initialize_extra_account_metas},
invoke,
},
std::sync::Arc,
};
fn keypair_clone(kp: &Keypair) -> Keypair {
Keypair::from_bytes(&kp.to_bytes()).expect("failed to copy keypair")
}
async fn setup(
program_id: &Pubkey,
) -> (
Arc<Mutex<ProgramTestContext>>,
Arc<dyn ProgramClient<ProgramBanksClientProcessTransaction>>,
Arc<Keypair>,
) {
let mut program_test = ProgramTest::new(
"spl_transfer_hook_example",
*program_id,
processor!(spl_transfer_hook_example::processor::process),
);
program_test.prefer_bpf(false); // simplicity in the build
program_test.add_program(
"spl_token_2022",
spl_token_2022::id(),
processor!(spl_token_2022::processor::Processor::process),
);
let context = program_test.start_with_context().await;
let payer = Arc::new(keypair_clone(&context.payer));
let context = Arc::new(Mutex::new(context));
let client: Arc<dyn ProgramClient<ProgramBanksClientProcessTransaction>> =
Arc::new(ProgramBanksClient::new_from_context(
Arc::clone(&context),
ProgramBanksClientProcessTransaction,
));
(context, client, payer)
}
async fn setup_mint<T: SendTransaction>(
program_id: &Pubkey,
mint_authority: &Pubkey,
decimals: u8,
payer: Arc<Keypair>,
client: Arc<dyn ProgramClient<T>>,
) -> Token<T> {
let mint_account = Keypair::new();
let token = Token::new(
client,
program_id,
&mint_account.pubkey(),
Some(decimals),
payer,
);
token
.create_mint(mint_authority, None, vec![], &[&mint_account])
.await
.unwrap();
token
}
#[tokio::test]
async fn success() {
let program_id = Pubkey::new_unique();
let (context, client, payer) = setup(&program_id).await;
let token_program_id = spl_token_2022::id();
let wallet = Keypair::new();
let mint_authority = Keypair::new();
let mint_authority_pubkey = mint_authority.pubkey();
let decimals = 2;
let token = setup_mint(
&token_program_id,
&mint_authority_pubkey,
decimals,
payer.clone(),
client.clone(),
)
.await;
let extra_account_metas = get_extra_account_metas_address(token.get_address(), &program_id);
token
.create_associated_token_account(&wallet.pubkey())
.await
.unwrap();
let source = token.get_associated_token_address(&wallet.pubkey());
let token_amount = 1_000_000_000_000;
token
.mint_to(
&source,
&mint_authority_pubkey,
token_amount,
&[&mint_authority],
)
.await
.unwrap();
let destination = Keypair::new();
token
.create_auxiliary_token_account(&destination, &wallet.pubkey())
.await
.unwrap();
let destination = destination.pubkey();
let extra_account_pubkeys = [
AccountMeta::new_readonly(sysvar::instructions::id(), false),
AccountMeta::new_readonly(mint_authority_pubkey, true),
AccountMeta::new(extra_account_metas, false),
];
let mut context = context.lock().await;
let rent = context.banks_client.get_rent().await.unwrap();
let rent_lamports =
rent.minimum_balance(ExtraAccountMetas::size_of(extra_account_pubkeys.len()).unwrap());
let transaction = Transaction::new_signed_with_payer(
&[
system_instruction::transfer(
&context.payer.pubkey(),
&extra_account_metas,
rent_lamports,
),
initialize_extra_account_metas(
&program_id,
&extra_account_metas,
token.get_address(),
&mint_authority_pubkey,
&extra_account_pubkeys,
),
],
Some(&context.payer.pubkey()),
&[&context.payer, &mint_authority],
context.last_blockhash,
);
context
.banks_client
.process_transaction(transaction)
.await
.unwrap();
// fail with missing account
{
let transaction = Transaction::new_signed_with_payer(
&[execute_with_extra_account_metas(
&program_id,
&source,
token.get_address(),
&destination,
&wallet.pubkey(),
&extra_account_metas,
&extra_account_pubkeys[..2],
0,
)],
Some(&context.payer.pubkey()),
&[&context.payer, &mint_authority],
context.last_blockhash,
);
let error = context
.banks_client
.process_transaction(transaction)
.await
.unwrap_err()
.unwrap();
assert_eq!(
error,
TransactionError::InstructionError(
0,
InstructionError::Custom(TransferHookError::IncorrectAccount as u32),
)
);
}
// fail with wrong account
{
let extra_account_pubkeys = [
AccountMeta::new_readonly(sysvar::instructions::id(), false),
AccountMeta::new_readonly(mint_authority_pubkey, true),
AccountMeta::new(wallet.pubkey(), false),
];
let transaction = Transaction::new_signed_with_payer(
&[execute_with_extra_account_metas(
&program_id,
&source,
token.get_address(),
&destination,
&wallet.pubkey(),
&extra_account_metas,
&extra_account_pubkeys,
0,
)],
Some(&context.payer.pubkey()),
&[&context.payer, &mint_authority],
context.last_blockhash,
);
let error = context
.banks_client
.process_transaction(transaction)
.await
.unwrap_err()
.unwrap();
assert_eq!(
error,
TransactionError::InstructionError(
0,
InstructionError::Custom(TransferHookError::IncorrectAccount as u32),
)
);
}
// fail with not signer
{
let extra_account_pubkeys = [
AccountMeta::new_readonly(sysvar::instructions::id(), false),
AccountMeta::new_readonly(mint_authority_pubkey, false),
AccountMeta::new(extra_account_metas, false),
];
let transaction = Transaction::new_signed_with_payer(
&[execute_with_extra_account_metas(
&program_id,
&source,
token.get_address(),
&destination,
&wallet.pubkey(),
&extra_account_metas,
&extra_account_pubkeys,
0,
)],
Some(&context.payer.pubkey()),
&[&context.payer],
context.last_blockhash,
);
let error = context
.banks_client
.process_transaction(transaction)
.await
.unwrap_err()
.unwrap();
assert_eq!(
error,
TransactionError::InstructionError(
0,
InstructionError::Custom(TransferHookError::IncorrectAccount as u32),
)
);
}
// success with correct params
{
let transaction = Transaction::new_signed_with_payer(
&[execute_with_extra_account_metas(
&program_id,
&source,
token.get_address(),
&destination,
&wallet.pubkey(),
&extra_account_metas,
&extra_account_pubkeys,
0,
)],
Some(&context.payer.pubkey()),
&[&context.payer, &mint_authority],
context.last_blockhash,
);
context
.banks_client
.process_transaction(transaction)
.await
.unwrap();
}
}
#[tokio::test]
async fn fail_incorrect_derivation() {
let program_id = Pubkey::new_unique();
let (context, client, payer) = setup(&program_id).await;
let token_program_id = spl_token_2022::id();
let mint_authority = Keypair::new();
let mint_authority_pubkey = mint_authority.pubkey();
let decimals = 2;
let token = setup_mint(
&token_program_id,
&mint_authority_pubkey,
decimals,
payer.clone(),
client.clone(),
)
.await;
// wrong derivation
let extra_account_metas = get_extra_account_metas_address(&program_id, token.get_address());
let mut context = context.lock().await;
let rent = context.banks_client.get_rent().await.unwrap();
let rent_lamports = rent.minimum_balance(ExtraAccountMetas::size_of(0).unwrap());
let transaction = Transaction::new_signed_with_payer(
&[
system_instruction::transfer(
&context.payer.pubkey(),
&extra_account_metas,
rent_lamports,
),
initialize_extra_account_metas(
&program_id,
&extra_account_metas,
token.get_address(),
&mint_authority_pubkey,
&[],
),
],
Some(&context.payer.pubkey()),
&[&context.payer, &mint_authority],
context.last_blockhash,
);
let error = context
.banks_client
.process_transaction(transaction)
.await
.unwrap_err()
.unwrap();
assert_eq!(
error,
TransactionError::InstructionError(1, InstructionError::InvalidSeeds)
);
}
/// Test program to CPI into default transfer-hook-interface program
pub fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
_input: &[u8],
) -> ProgramResult {
invoke::execute(
accounts[0].key,
accounts[1].clone(),
accounts[2].clone(),
accounts[3].clone(),
accounts[4].clone(),
&accounts[4..],
0,
)
}
#[tokio::test]
async fn success_on_chain_invoke() {
let hook_program_id = Pubkey::new_unique();
let mut program_test = ProgramTest::new(
"spl_transfer_hook_example",
hook_program_id,
processor!(spl_transfer_hook_example::processor::process),
);
program_test.prefer_bpf(false);
program_test.add_program(
"spl_token_2022",
spl_token_2022::id(),
processor!(spl_token_2022::processor::Processor::process),
);
let program_id = Pubkey::new_unique();
program_test.add_program(
"test_cpi_program",
program_id,
processor!(process_instruction),
);
let context = program_test.start_with_context().await;
let payer = Arc::new(keypair_clone(&context.payer));
let context = Arc::new(Mutex::new(context));
let client: Arc<dyn ProgramClient<ProgramBanksClientProcessTransaction>> =
Arc::new(ProgramBanksClient::new_from_context(
Arc::clone(&context),
ProgramBanksClientProcessTransaction,
));
let token_program_id = spl_token_2022::id();
let wallet = Keypair::new();
let mint_authority = Keypair::new();
let mint_authority_pubkey = mint_authority.pubkey();
let decimals = 2;
let token = setup_mint(
&token_program_id,
&mint_authority_pubkey,
decimals,
payer.clone(),
client.clone(),
)
.await;
let extra_account_metas =
get_extra_account_metas_address(token.get_address(), &hook_program_id);
let source = Pubkey::new_unique();
let destination = Pubkey::new_unique();
let extra_account_pubkeys = [
AccountMeta::new_readonly(sysvar::instructions::id(), false),
AccountMeta::new_readonly(mint_authority_pubkey, true),
AccountMeta::new(extra_account_metas, false),
];
let mut context = context.lock().await;
let rent = context.banks_client.get_rent().await.unwrap();
let rent_lamports =
rent.minimum_balance(ExtraAccountMetas::size_of(extra_account_pubkeys.len()).unwrap());
let transaction = Transaction::new_signed_with_payer(
&[
system_instruction::transfer(
&context.payer.pubkey(),
&extra_account_metas,
rent_lamports,
),
initialize_extra_account_metas(
&hook_program_id,
&extra_account_metas,
token.get_address(),
&mint_authority_pubkey,
&extra_account_pubkeys,
),
],
Some(&context.payer.pubkey()),
&[&context.payer, &mint_authority],
context.last_blockhash,
);
context
.banks_client
.process_transaction(transaction)
.await
.unwrap();
// easier to hack this up!
let mut test_instruction = execute_with_extra_account_metas(
&program_id,
&source,
token.get_address(),
&destination,
&wallet.pubkey(),
&extra_account_metas,
&extra_account_pubkeys,
0,
);
test_instruction
.accounts
.insert(0, AccountMeta::new_readonly(hook_program_id, false));
let transaction = Transaction::new_signed_with_payer(
&[test_instruction],
Some(&context.payer.pubkey()),
&[&context.payer, &mint_authority],
context.last_blockhash,
);
context
.banks_client
.process_transaction(transaction)
.await
.unwrap();
}