associated-token-account: Add recover nested account ix (#2889)
* associated-token-account: Add transfer / close nested accounts * Swap wallet and wrong_wallet in test * Use new error * Force destination to wallet and ATA * Fix merge conflicts * Add more vanilla spl-token tests * Improve test, fix instruction comments * Address feedback * Rename CloseNested -> RecoverNested, add comment * Fix typo in comment
This commit is contained in:
parent
b7a3fc6243
commit
182c0532e4
|
@ -34,6 +34,25 @@ pub enum AssociatedTokenAccountInstruction {
|
||||||
/// 4. `[]` System program
|
/// 4. `[]` System program
|
||||||
/// 5. `[]` SPL Token program
|
/// 5. `[]` SPL Token program
|
||||||
CreateIdempotent,
|
CreateIdempotent,
|
||||||
|
/// Transfers from and closes a nested associated token account: an
|
||||||
|
/// associated token account owned by an associated token account.
|
||||||
|
///
|
||||||
|
/// The tokens are moved from the nested associated token account to the
|
||||||
|
/// wallet's associated token account, and the nested account lamports are
|
||||||
|
/// moved to the wallet.
|
||||||
|
///
|
||||||
|
/// Note: Nested token accounts are an anti-pattern, and almost always
|
||||||
|
/// created unintentionally, so this instruction should only be used to
|
||||||
|
/// recover from errors.
|
||||||
|
///
|
||||||
|
/// 0. `[writeable]` Nested associated token account, must be owned by `3`
|
||||||
|
/// 1. `[]` Token mint for the nested associated token account
|
||||||
|
/// 2. `[writeable]` Wallet's associated token account
|
||||||
|
/// 3. `[]` Owner associated token account address, must be owned by `5`
|
||||||
|
/// 4. `[]` Token mint for the owner associated token account
|
||||||
|
/// 5. `[writeable, signer]` Wallet address for the owner associated token account
|
||||||
|
/// 6. `[]` SPL Token program
|
||||||
|
RecoverNested,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_associated_token_account_instruction(
|
fn build_associated_token_account_instruction(
|
||||||
|
@ -99,3 +118,43 @@ pub fn create_associated_token_account_idempotent(
|
||||||
AssociatedTokenAccountInstruction::CreateIdempotent,
|
AssociatedTokenAccountInstruction::CreateIdempotent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a `RecoverNested` instruction
|
||||||
|
pub fn recover_nested(
|
||||||
|
wallet_address: &Pubkey,
|
||||||
|
owner_token_mint_address: &Pubkey,
|
||||||
|
nested_token_mint_address: &Pubkey,
|
||||||
|
token_program_id: &Pubkey,
|
||||||
|
) -> Instruction {
|
||||||
|
let owner_associated_account_address = get_associated_token_address_with_program_id(
|
||||||
|
wallet_address,
|
||||||
|
owner_token_mint_address,
|
||||||
|
token_program_id,
|
||||||
|
);
|
||||||
|
let destination_associated_account_address = get_associated_token_address_with_program_id(
|
||||||
|
wallet_address,
|
||||||
|
nested_token_mint_address,
|
||||||
|
token_program_id,
|
||||||
|
);
|
||||||
|
let nested_associated_account_address = get_associated_token_address_with_program_id(
|
||||||
|
&owner_associated_account_address, // ATA is wrongly used as a wallet_address
|
||||||
|
nested_token_mint_address,
|
||||||
|
token_program_id,
|
||||||
|
);
|
||||||
|
|
||||||
|
let instruction_data = AssociatedTokenAccountInstruction::RecoverNested;
|
||||||
|
|
||||||
|
Instruction {
|
||||||
|
program_id: id(),
|
||||||
|
accounts: vec![
|
||||||
|
AccountMeta::new(nested_associated_account_address, false),
|
||||||
|
AccountMeta::new_readonly(*nested_token_mint_address, false),
|
||||||
|
AccountMeta::new(destination_associated_account_address, false),
|
||||||
|
AccountMeta::new_readonly(owner_associated_account_address, false),
|
||||||
|
AccountMeta::new_readonly(*owner_token_mint_address, false),
|
||||||
|
AccountMeta::new(*wallet_address, true),
|
||||||
|
AccountMeta::new_readonly(*token_program_id, false),
|
||||||
|
],
|
||||||
|
data: instruction_data.try_to_vec().unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ use {
|
||||||
account_info::{next_account_info, AccountInfo},
|
account_info::{next_account_info, AccountInfo},
|
||||||
entrypoint::ProgramResult,
|
entrypoint::ProgramResult,
|
||||||
msg,
|
msg,
|
||||||
program::invoke,
|
program::{invoke, invoke_signed},
|
||||||
program_error::ProgramError,
|
program_error::ProgramError,
|
||||||
pubkey::Pubkey,
|
pubkey::Pubkey,
|
||||||
rent::Rent,
|
rent::Rent,
|
||||||
|
@ -21,7 +21,7 @@ use {
|
||||||
},
|
},
|
||||||
spl_token_2022::{
|
spl_token_2022::{
|
||||||
extension::{ExtensionType, StateWithExtensions},
|
extension::{ExtensionType, StateWithExtensions},
|
||||||
state::Account,
|
state::{Account, Mint},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -56,6 +56,9 @@ pub fn process_instruction(
|
||||||
AssociatedTokenAccountInstruction::CreateIdempotent => {
|
AssociatedTokenAccountInstruction::CreateIdempotent => {
|
||||||
process_create_associated_token_account(program_id, accounts, CreateMode::Idempotent)
|
process_create_associated_token_account(program_id, accounts, CreateMode::Idempotent)
|
||||||
}
|
}
|
||||||
|
AssociatedTokenAccountInstruction::RecoverNested => {
|
||||||
|
process_recover_nested(program_id, accounts)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,3 +160,150 @@ fn process_create_associated_token_account(
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Processes `RecoverNested` instruction
|
||||||
|
pub fn process_recover_nested(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
|
||||||
|
let account_info_iter = &mut accounts.iter();
|
||||||
|
|
||||||
|
let nested_associated_token_account_info = next_account_info(account_info_iter)?;
|
||||||
|
let nested_token_mint_info = next_account_info(account_info_iter)?;
|
||||||
|
let destination_associated_token_account_info = next_account_info(account_info_iter)?;
|
||||||
|
let owner_associated_token_account_info = next_account_info(account_info_iter)?;
|
||||||
|
let owner_token_mint_info = next_account_info(account_info_iter)?;
|
||||||
|
let wallet_account_info = next_account_info(account_info_iter)?;
|
||||||
|
let spl_token_program_info = next_account_info(account_info_iter)?;
|
||||||
|
let spl_token_program_id = spl_token_program_info.key;
|
||||||
|
|
||||||
|
// Check owner address derivation
|
||||||
|
let (owner_associated_token_address, bump_seed) =
|
||||||
|
get_associated_token_address_and_bump_seed_internal(
|
||||||
|
wallet_account_info.key,
|
||||||
|
owner_token_mint_info.key,
|
||||||
|
program_id,
|
||||||
|
spl_token_program_id,
|
||||||
|
);
|
||||||
|
if owner_associated_token_address != *owner_associated_token_account_info.key {
|
||||||
|
msg!("Error: Owner associated address does not match seed derivation");
|
||||||
|
return Err(ProgramError::InvalidSeeds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check nested address derivation
|
||||||
|
let (nested_associated_token_address, _) = get_associated_token_address_and_bump_seed_internal(
|
||||||
|
owner_associated_token_account_info.key,
|
||||||
|
nested_token_mint_info.key,
|
||||||
|
program_id,
|
||||||
|
spl_token_program_id,
|
||||||
|
);
|
||||||
|
if nested_associated_token_address != *nested_associated_token_account_info.key {
|
||||||
|
msg!("Error: Nested associated address does not match seed derivation");
|
||||||
|
return Err(ProgramError::InvalidSeeds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check destination address derivation
|
||||||
|
let (destination_associated_token_address, _) =
|
||||||
|
get_associated_token_address_and_bump_seed_internal(
|
||||||
|
wallet_account_info.key,
|
||||||
|
nested_token_mint_info.key,
|
||||||
|
program_id,
|
||||||
|
spl_token_program_id,
|
||||||
|
);
|
||||||
|
if destination_associated_token_address != *destination_associated_token_account_info.key {
|
||||||
|
msg!("Error: Destination associated address does not match seed derivation");
|
||||||
|
return Err(ProgramError::InvalidSeeds);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !wallet_account_info.is_signer {
|
||||||
|
msg!("Wallet of the owner associated token account must sign");
|
||||||
|
return Err(ProgramError::MissingRequiredSignature);
|
||||||
|
}
|
||||||
|
|
||||||
|
if owner_token_mint_info.owner != spl_token_program_id {
|
||||||
|
msg!("Owner mint not owned by provided token program");
|
||||||
|
return Err(ProgramError::IllegalOwner);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Account data is dropped at the end of this, so the CPI can succeed
|
||||||
|
// without a double-borrow
|
||||||
|
let (amount, decimals) = {
|
||||||
|
// Check owner associated token account data
|
||||||
|
if owner_associated_token_account_info.owner != spl_token_program_id {
|
||||||
|
msg!("Owner associated token account not owned by provided token program, recreate the owner associated token account first");
|
||||||
|
return Err(ProgramError::IllegalOwner);
|
||||||
|
}
|
||||||
|
let owner_account_data = owner_associated_token_account_info.data.borrow();
|
||||||
|
let owner_account = StateWithExtensions::<Account>::unpack(&owner_account_data)?;
|
||||||
|
if owner_account.base.owner != *wallet_account_info.key {
|
||||||
|
msg!("Owner associated token account not owned by provided wallet");
|
||||||
|
return Err(AssociatedTokenAccountError::InvalidOwner.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check nested associated token account data
|
||||||
|
if nested_associated_token_account_info.owner != spl_token_program_id {
|
||||||
|
msg!("Nested associated token account not owned by provided token program");
|
||||||
|
return Err(ProgramError::IllegalOwner);
|
||||||
|
}
|
||||||
|
let nested_account_data = nested_associated_token_account_info.data.borrow();
|
||||||
|
let nested_account = StateWithExtensions::<Account>::unpack(&nested_account_data)?;
|
||||||
|
if nested_account.base.owner != *owner_associated_token_account_info.key {
|
||||||
|
msg!("Nested associated token account not owned by provided associated token account");
|
||||||
|
return Err(AssociatedTokenAccountError::InvalidOwner.into());
|
||||||
|
}
|
||||||
|
let amount = nested_account.base.amount;
|
||||||
|
|
||||||
|
// Check nested token mint data
|
||||||
|
if nested_token_mint_info.owner != spl_token_program_id {
|
||||||
|
msg!("Nested mint account not owned by provided token program");
|
||||||
|
return Err(ProgramError::IllegalOwner);
|
||||||
|
}
|
||||||
|
let nested_mint_data = nested_token_mint_info.data.borrow();
|
||||||
|
let nested_mint = StateWithExtensions::<Mint>::unpack(&nested_mint_data)?;
|
||||||
|
let decimals = nested_mint.base.decimals;
|
||||||
|
(amount, decimals)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Transfer everything out
|
||||||
|
let owner_associated_token_account_signer_seeds: &[&[_]] = &[
|
||||||
|
&wallet_account_info.key.to_bytes(),
|
||||||
|
&spl_token_program_id.to_bytes(),
|
||||||
|
&owner_token_mint_info.key.to_bytes(),
|
||||||
|
&[bump_seed],
|
||||||
|
];
|
||||||
|
invoke_signed(
|
||||||
|
&spl_token_2022::instruction::transfer_checked(
|
||||||
|
spl_token_program_id,
|
||||||
|
nested_associated_token_account_info.key,
|
||||||
|
nested_token_mint_info.key,
|
||||||
|
destination_associated_token_account_info.key,
|
||||||
|
owner_associated_token_account_info.key,
|
||||||
|
&[],
|
||||||
|
amount,
|
||||||
|
decimals,
|
||||||
|
)?,
|
||||||
|
&[
|
||||||
|
nested_associated_token_account_info.clone(),
|
||||||
|
nested_token_mint_info.clone(),
|
||||||
|
destination_associated_token_account_info.clone(),
|
||||||
|
owner_associated_token_account_info.clone(),
|
||||||
|
spl_token_program_info.clone(),
|
||||||
|
],
|
||||||
|
&[owner_associated_token_account_signer_seeds],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Close the nested account so it's never used again
|
||||||
|
invoke_signed(
|
||||||
|
&spl_token_2022::instruction::close_account(
|
||||||
|
spl_token_program_id,
|
||||||
|
nested_associated_token_account_info.key,
|
||||||
|
wallet_account_info.key,
|
||||||
|
owner_associated_token_account_info.key,
|
||||||
|
&[],
|
||||||
|
)?,
|
||||||
|
&[
|
||||||
|
nested_associated_token_account_info.clone(),
|
||||||
|
wallet_account_info.clone(),
|
||||||
|
owner_associated_token_account_info.clone(),
|
||||||
|
spl_token_program_info.clone(),
|
||||||
|
],
|
||||||
|
&[owner_associated_token_account_signer_seeds],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
mod program_test;
|
mod program_test;
|
||||||
|
|
||||||
use {
|
use {
|
||||||
program_test::program_test,
|
program_test::program_test_2022,
|
||||||
solana_program::{instruction::*, pubkey::Pubkey},
|
solana_program::{instruction::*, pubkey::Pubkey},
|
||||||
solana_program_test::*,
|
solana_program_test::*,
|
||||||
solana_sdk::{
|
solana_sdk::{
|
||||||
|
@ -40,7 +40,7 @@ async fn success_account_exists() {
|
||||||
);
|
);
|
||||||
|
|
||||||
let (mut banks_client, payer, recent_blockhash) =
|
let (mut banks_client, payer, recent_blockhash) =
|
||||||
program_test(token_mint_address, true).start().await;
|
program_test_2022(token_mint_address, true).start().await;
|
||||||
let rent = banks_client.get_rent().await.unwrap();
|
let rent = banks_client.get_rent().await.unwrap();
|
||||||
let expected_token_account_len =
|
let expected_token_account_len =
|
||||||
ExtensionType::get_account_len::<Account>(&[ExtensionType::ImmutableOwner]);
|
ExtensionType::get_account_len::<Account>(&[ExtensionType::ImmutableOwner]);
|
||||||
|
@ -150,7 +150,7 @@ async fn fail_account_exists_with_wrong_owner() {
|
||||||
close_authority: COption::None,
|
close_authority: COption::None,
|
||||||
};
|
};
|
||||||
Account::pack(token_account, &mut associated_token_account.data).unwrap();
|
Account::pack(token_account, &mut associated_token_account.data).unwrap();
|
||||||
let mut pt = program_test(token_mint_address, true);
|
let mut pt = program_test_2022(token_mint_address, true);
|
||||||
pt.add_account(associated_token_address, associated_token_account);
|
pt.add_account(associated_token_address, associated_token_account);
|
||||||
let (mut banks_client, payer, recent_blockhash) = pt.start().await;
|
let (mut banks_client, payer, recent_blockhash) = pt.start().await;
|
||||||
|
|
||||||
|
@ -185,7 +185,7 @@ async fn fail_account_exists_with_wrong_owner() {
|
||||||
async fn fail_non_ata() {
|
async fn fail_non_ata() {
|
||||||
let token_mint_address = Pubkey::new_unique();
|
let token_mint_address = Pubkey::new_unique();
|
||||||
let (mut banks_client, payer, recent_blockhash) =
|
let (mut banks_client, payer, recent_blockhash) =
|
||||||
program_test(token_mint_address, true).start().await;
|
program_test_2022(token_mint_address, true).start().await;
|
||||||
|
|
||||||
let rent = banks_client.get_rent().await.unwrap();
|
let rent = banks_client.get_rent().await.unwrap();
|
||||||
let token_account_len =
|
let token_account_len =
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
mod program_test;
|
mod program_test;
|
||||||
|
|
||||||
use {
|
use {
|
||||||
program_test::program_test,
|
program_test::program_test_2022,
|
||||||
solana_program::{instruction::*, pubkey::Pubkey, system_instruction},
|
solana_program::{instruction::*, pubkey::Pubkey, system_instruction},
|
||||||
solana_program_test::*,
|
solana_program_test::*,
|
||||||
solana_sdk::{
|
solana_sdk::{
|
||||||
|
@ -29,7 +29,7 @@ async fn test_associated_token_account_with_transfer_fees() {
|
||||||
let wallet_receiver = Keypair::new();
|
let wallet_receiver = Keypair::new();
|
||||||
let wallet_address_receiver = wallet_receiver.pubkey();
|
let wallet_address_receiver = wallet_receiver.pubkey();
|
||||||
let (mut banks_client, payer, recent_blockhash) =
|
let (mut banks_client, payer, recent_blockhash) =
|
||||||
program_test(Pubkey::new_unique(), true).start().await;
|
program_test_2022(Pubkey::new_unique(), true).start().await;
|
||||||
let rent = banks_client.get_rent().await.unwrap();
|
let rent = banks_client.get_rent().await.unwrap();
|
||||||
|
|
||||||
// create extended mint
|
// create extended mint
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
mod program_test;
|
mod program_test;
|
||||||
|
|
||||||
use {
|
use {
|
||||||
program_test::program_test,
|
program_test::program_test_2022,
|
||||||
solana_program::{instruction::*, pubkey::Pubkey, system_instruction, sysvar},
|
solana_program::{instruction::*, pubkey::Pubkey, system_instruction, sysvar},
|
||||||
solana_program_test::*,
|
solana_program_test::*,
|
||||||
solana_sdk::{
|
solana_sdk::{
|
||||||
|
@ -28,7 +28,7 @@ async fn test_associated_token_address() {
|
||||||
);
|
);
|
||||||
|
|
||||||
let (mut banks_client, payer, recent_blockhash) =
|
let (mut banks_client, payer, recent_blockhash) =
|
||||||
program_test(token_mint_address, true).start().await;
|
program_test_2022(token_mint_address, true).start().await;
|
||||||
let rent = banks_client.get_rent().await.unwrap();
|
let rent = banks_client.get_rent().await.unwrap();
|
||||||
|
|
||||||
let expected_token_account_len =
|
let expected_token_account_len =
|
||||||
|
@ -78,7 +78,7 @@ async fn test_create_with_fewer_lamports() {
|
||||||
);
|
);
|
||||||
|
|
||||||
let (mut banks_client, payer, recent_blockhash) =
|
let (mut banks_client, payer, recent_blockhash) =
|
||||||
program_test(token_mint_address, true).start().await;
|
program_test_2022(token_mint_address, true).start().await;
|
||||||
let rent = banks_client.get_rent().await.unwrap();
|
let rent = banks_client.get_rent().await.unwrap();
|
||||||
let expected_token_account_len =
|
let expected_token_account_len =
|
||||||
ExtensionType::get_account_len::<Account>(&[ExtensionType::ImmutableOwner]);
|
ExtensionType::get_account_len::<Account>(&[ExtensionType::ImmutableOwner]);
|
||||||
|
@ -138,7 +138,7 @@ async fn test_create_with_excess_lamports() {
|
||||||
);
|
);
|
||||||
|
|
||||||
let (mut banks_client, payer, recent_blockhash) =
|
let (mut banks_client, payer, recent_blockhash) =
|
||||||
program_test(token_mint_address, true).start().await;
|
program_test_2022(token_mint_address, true).start().await;
|
||||||
let rent = banks_client.get_rent().await.unwrap();
|
let rent = banks_client.get_rent().await.unwrap();
|
||||||
|
|
||||||
let expected_token_account_len =
|
let expected_token_account_len =
|
||||||
|
@ -198,7 +198,7 @@ async fn test_create_account_mismatch() {
|
||||||
);
|
);
|
||||||
|
|
||||||
let (mut banks_client, payer, recent_blockhash) =
|
let (mut banks_client, payer, recent_blockhash) =
|
||||||
program_test(token_mint_address, true).start().await;
|
program_test_2022(token_mint_address, true).start().await;
|
||||||
|
|
||||||
let mut instruction = create_associated_token_account(
|
let mut instruction = create_associated_token_account(
|
||||||
&payer.pubkey(),
|
&payer.pubkey(),
|
||||||
|
@ -269,7 +269,7 @@ async fn test_create_associated_token_account_using_legacy_implicit_instruction(
|
||||||
);
|
);
|
||||||
|
|
||||||
let (mut banks_client, payer, recent_blockhash) =
|
let (mut banks_client, payer, recent_blockhash) =
|
||||||
program_test(token_mint_address, true).start().await;
|
program_test_2022(token_mint_address, true).start().await;
|
||||||
let rent = banks_client.get_rent().await.unwrap();
|
let rent = banks_client.get_rent().await.unwrap();
|
||||||
let expected_token_account_len =
|
let expected_token_account_len =
|
||||||
ExtensionType::get_account_len::<Account>(&[ExtensionType::ImmutableOwner]);
|
ExtensionType::get_account_len::<Account>(&[ExtensionType::ImmutableOwner]);
|
||||||
|
|
|
@ -4,6 +4,7 @@ use {
|
||||||
spl_associated_token_account::{id, processor::process_instruction},
|
spl_associated_token_account::{id, processor::process_instruction},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn program_test(token_mint_address: Pubkey, use_latest_spl_token: bool) -> ProgramTest {
|
pub fn program_test(token_mint_address: Pubkey, use_latest_spl_token: bool) -> ProgramTest {
|
||||||
let mut pc = ProgramTest::new(
|
let mut pc = ProgramTest::new(
|
||||||
"spl_associated_token_account",
|
"spl_associated_token_account",
|
||||||
|
@ -12,6 +13,45 @@ pub fn program_test(token_mint_address: Pubkey, use_latest_spl_token: bool) -> P
|
||||||
);
|
);
|
||||||
|
|
||||||
if use_latest_spl_token {
|
if use_latest_spl_token {
|
||||||
|
// TODO: Remove when spl-token is available by default in program-test
|
||||||
|
pc.add_program(
|
||||||
|
"spl_token",
|
||||||
|
spl_token::id(),
|
||||||
|
processor!(spl_token::processor::Processor::process),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a token mint account
|
||||||
|
//
|
||||||
|
// The account data was generated by running:
|
||||||
|
// $ solana account EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v \
|
||||||
|
// --output-file tests/fixtures/token-mint-data.bin
|
||||||
|
//
|
||||||
|
pc.add_account_with_file_data(
|
||||||
|
token_mint_address,
|
||||||
|
1461600,
|
||||||
|
spl_token::id(),
|
||||||
|
"token-mint-data.bin",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Dial down the BPF compute budget to detect if the program gets bloated in the future
|
||||||
|
pc.set_compute_max_units(50_000);
|
||||||
|
|
||||||
|
pc
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn program_test_2022(
|
||||||
|
token_mint_address: Pubkey,
|
||||||
|
use_latest_spl_token_2022: bool,
|
||||||
|
) -> ProgramTest {
|
||||||
|
let mut pc = ProgramTest::new(
|
||||||
|
"spl_associated_token_account",
|
||||||
|
id(),
|
||||||
|
processor!(process_instruction),
|
||||||
|
);
|
||||||
|
|
||||||
|
if use_latest_spl_token_2022 {
|
||||||
// TODO: Remove when spl-token-2022 is available by default in program-test
|
// TODO: Remove when spl-token-2022 is available by default in program-test
|
||||||
pc.add_program(
|
pc.add_program(
|
||||||
"spl_token_2022",
|
"spl_token_2022",
|
||||||
|
|
|
@ -0,0 +1,632 @@
|
||||||
|
// Mark this test as BPF-only due to current `ProgramTest` limitations when CPIing into the system program
|
||||||
|
#![cfg(feature = "test-bpf")]
|
||||||
|
|
||||||
|
mod program_test;
|
||||||
|
|
||||||
|
use {
|
||||||
|
program_test::{program_test, program_test_2022},
|
||||||
|
solana_program::{pubkey::Pubkey, system_instruction},
|
||||||
|
solana_program_test::*,
|
||||||
|
solana_sdk::{
|
||||||
|
instruction::{AccountMeta, InstructionError},
|
||||||
|
signature::Signer,
|
||||||
|
signer::keypair::Keypair,
|
||||||
|
transaction::{Transaction, TransactionError},
|
||||||
|
},
|
||||||
|
spl_associated_token_account::{get_associated_token_address_with_program_id, instruction},
|
||||||
|
spl_token_2022::{
|
||||||
|
extension::{ExtensionType, StateWithExtensionsOwned},
|
||||||
|
state::{Account, Mint},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async fn create_mint(context: &mut ProgramTestContext, program_id: &Pubkey) -> (Pubkey, Keypair) {
|
||||||
|
let mint_account = Keypair::new();
|
||||||
|
let token_mint_address = mint_account.pubkey();
|
||||||
|
let mint_authority = Keypair::new();
|
||||||
|
let space = ExtensionType::get_account_len::<Mint>(&[]);
|
||||||
|
let rent = context.banks_client.get_rent().await.unwrap();
|
||||||
|
let transaction = Transaction::new_signed_with_payer(
|
||||||
|
&[
|
||||||
|
system_instruction::create_account(
|
||||||
|
&context.payer.pubkey(),
|
||||||
|
&mint_account.pubkey(),
|
||||||
|
rent.minimum_balance(space),
|
||||||
|
space as u64,
|
||||||
|
program_id,
|
||||||
|
),
|
||||||
|
spl_token_2022::instruction::initialize_mint(
|
||||||
|
program_id,
|
||||||
|
&token_mint_address,
|
||||||
|
&mint_authority.pubkey(),
|
||||||
|
Some(&mint_authority.pubkey()),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
],
|
||||||
|
Some(&context.payer.pubkey()),
|
||||||
|
&[&context.payer, &mint_account],
|
||||||
|
context.last_blockhash,
|
||||||
|
);
|
||||||
|
context
|
||||||
|
.banks_client
|
||||||
|
.process_transaction(transaction)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
(token_mint_address, mint_authority)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_associated_token_account(
|
||||||
|
context: &mut ProgramTestContext,
|
||||||
|
owner: &Pubkey,
|
||||||
|
mint: &Pubkey,
|
||||||
|
program_id: &Pubkey,
|
||||||
|
) -> Pubkey {
|
||||||
|
let transaction = Transaction::new_signed_with_payer(
|
||||||
|
&[instruction::create_associated_token_account(
|
||||||
|
&context.payer.pubkey(),
|
||||||
|
owner,
|
||||||
|
mint,
|
||||||
|
program_id,
|
||||||
|
)],
|
||||||
|
Some(&context.payer.pubkey()),
|
||||||
|
&[&context.payer],
|
||||||
|
context.last_blockhash,
|
||||||
|
);
|
||||||
|
context
|
||||||
|
.banks_client
|
||||||
|
.process_transaction(transaction)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
get_associated_token_address_with_program_id(owner, mint, program_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
async fn try_recover_nested(
|
||||||
|
context: &mut ProgramTestContext,
|
||||||
|
program_id: &Pubkey,
|
||||||
|
nested_mint: Pubkey,
|
||||||
|
nested_mint_authority: Keypair,
|
||||||
|
nested_associated_token_address: Pubkey,
|
||||||
|
destination_token_address: Pubkey,
|
||||||
|
wallet: Keypair,
|
||||||
|
recover_transaction: Transaction,
|
||||||
|
expected_error: Option<InstructionError>,
|
||||||
|
) {
|
||||||
|
let nested_account = context
|
||||||
|
.banks_client
|
||||||
|
.get_account(nested_associated_token_address)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
let lamports = nested_account.lamports;
|
||||||
|
|
||||||
|
// mint to nested account
|
||||||
|
let amount = 100;
|
||||||
|
let transaction = Transaction::new_signed_with_payer(
|
||||||
|
&[spl_token_2022::instruction::mint_to(
|
||||||
|
program_id,
|
||||||
|
&nested_mint,
|
||||||
|
&nested_associated_token_address,
|
||||||
|
&nested_mint_authority.pubkey(),
|
||||||
|
&[],
|
||||||
|
amount,
|
||||||
|
)
|
||||||
|
.unwrap()],
|
||||||
|
Some(&context.payer.pubkey()),
|
||||||
|
&[&context.payer, &nested_mint_authority],
|
||||||
|
context.last_blockhash,
|
||||||
|
);
|
||||||
|
context
|
||||||
|
.banks_client
|
||||||
|
.process_transaction(transaction)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// transfer / close nested account
|
||||||
|
let result = context
|
||||||
|
.banks_client
|
||||||
|
.process_transaction(recover_transaction)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Some(expected_error) = expected_error {
|
||||||
|
let error = result.unwrap_err().unwrap();
|
||||||
|
assert_eq!(error, TransactionError::InstructionError(0, expected_error));
|
||||||
|
} else {
|
||||||
|
result.unwrap();
|
||||||
|
// nested account is gone
|
||||||
|
assert!(context
|
||||||
|
.banks_client
|
||||||
|
.get_account(nested_associated_token_address)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.is_none());
|
||||||
|
let destination_account = context
|
||||||
|
.banks_client
|
||||||
|
.get_account(destination_token_address)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
let destination_state =
|
||||||
|
StateWithExtensionsOwned::<Account>::unpack(destination_account.data).unwrap();
|
||||||
|
assert_eq!(destination_state.base.amount, amount);
|
||||||
|
let wallet_account = context
|
||||||
|
.banks_client
|
||||||
|
.get_account(wallet.pubkey())
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(wallet_account.lamports, lamports);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_same_mint(context: &mut ProgramTestContext, program_id: &Pubkey) {
|
||||||
|
let wallet = Keypair::new();
|
||||||
|
let (mint, mint_authority) = create_mint(context, program_id).await;
|
||||||
|
|
||||||
|
let owner_associated_token_address =
|
||||||
|
create_associated_token_account(context, &wallet.pubkey(), &mint, program_id).await;
|
||||||
|
let nested_associated_token_address = create_associated_token_account(
|
||||||
|
context,
|
||||||
|
&owner_associated_token_address,
|
||||||
|
&mint,
|
||||||
|
program_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let transaction = Transaction::new_signed_with_payer(
|
||||||
|
&[instruction::recover_nested(
|
||||||
|
&wallet.pubkey(),
|
||||||
|
&mint,
|
||||||
|
&mint,
|
||||||
|
program_id,
|
||||||
|
)],
|
||||||
|
Some(&context.payer.pubkey()),
|
||||||
|
&[&context.payer, &wallet],
|
||||||
|
context.last_blockhash,
|
||||||
|
);
|
||||||
|
try_recover_nested(
|
||||||
|
context,
|
||||||
|
program_id,
|
||||||
|
mint,
|
||||||
|
mint_authority,
|
||||||
|
nested_associated_token_address,
|
||||||
|
owner_associated_token_address,
|
||||||
|
wallet,
|
||||||
|
transaction,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn success_same_mint_2022() {
|
||||||
|
let dummy_mint = Pubkey::new_unique();
|
||||||
|
let pt = program_test_2022(dummy_mint, true);
|
||||||
|
let mut context = pt.start_with_context().await;
|
||||||
|
check_same_mint(&mut context, &spl_token_2022::id()).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn success_same_mint() {
|
||||||
|
let dummy_mint = Pubkey::new_unique();
|
||||||
|
let pt = program_test(dummy_mint, true);
|
||||||
|
let mut context = pt.start_with_context().await;
|
||||||
|
check_same_mint(&mut context, &spl_token::id()).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_different_mints(context: &mut ProgramTestContext, program_id: &Pubkey) {
|
||||||
|
let wallet = Keypair::new();
|
||||||
|
let (owner_mint, _owner_mint_authority) = create_mint(context, program_id).await;
|
||||||
|
let (nested_mint, nested_mint_authority) = create_mint(context, program_id).await;
|
||||||
|
|
||||||
|
let owner_associated_token_address =
|
||||||
|
create_associated_token_account(context, &wallet.pubkey(), &owner_mint, program_id).await;
|
||||||
|
let nested_associated_token_address = create_associated_token_account(
|
||||||
|
context,
|
||||||
|
&owner_associated_token_address,
|
||||||
|
&nested_mint,
|
||||||
|
program_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let destination_token_address =
|
||||||
|
create_associated_token_account(context, &wallet.pubkey(), &nested_mint, program_id).await;
|
||||||
|
|
||||||
|
let transaction = Transaction::new_signed_with_payer(
|
||||||
|
&[instruction::recover_nested(
|
||||||
|
&wallet.pubkey(),
|
||||||
|
&owner_mint,
|
||||||
|
&nested_mint,
|
||||||
|
program_id,
|
||||||
|
)],
|
||||||
|
Some(&context.payer.pubkey()),
|
||||||
|
&[&context.payer, &wallet],
|
||||||
|
context.last_blockhash,
|
||||||
|
);
|
||||||
|
try_recover_nested(
|
||||||
|
context,
|
||||||
|
program_id,
|
||||||
|
nested_mint,
|
||||||
|
nested_mint_authority,
|
||||||
|
nested_associated_token_address,
|
||||||
|
destination_token_address,
|
||||||
|
wallet,
|
||||||
|
transaction,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn success_different_mints() {
|
||||||
|
let dummy_mint = Pubkey::new_unique();
|
||||||
|
let pt = program_test(dummy_mint, true);
|
||||||
|
let mut context = pt.start_with_context().await;
|
||||||
|
check_different_mints(&mut context, &spl_token::id()).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn success_different_mints_2022() {
|
||||||
|
let dummy_mint = Pubkey::new_unique();
|
||||||
|
let pt = program_test_2022(dummy_mint, true);
|
||||||
|
let mut context = pt.start_with_context().await;
|
||||||
|
check_different_mints(&mut context, &spl_token_2022::id()).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_missing_wallet_signature(context: &mut ProgramTestContext, program_id: &Pubkey) {
|
||||||
|
let wallet = Keypair::new();
|
||||||
|
let (mint, mint_authority) = create_mint(context, program_id).await;
|
||||||
|
|
||||||
|
let owner_associated_token_address =
|
||||||
|
create_associated_token_account(context, &wallet.pubkey(), &mint, program_id).await;
|
||||||
|
|
||||||
|
let nested_associated_token_address = create_associated_token_account(
|
||||||
|
context,
|
||||||
|
&owner_associated_token_address,
|
||||||
|
&mint,
|
||||||
|
program_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut recover = instruction::recover_nested(&wallet.pubkey(), &mint, &mint, program_id);
|
||||||
|
recover.accounts[5] = AccountMeta::new(wallet.pubkey(), false);
|
||||||
|
let transaction = Transaction::new_signed_with_payer(
|
||||||
|
&[recover],
|
||||||
|
Some(&context.payer.pubkey()),
|
||||||
|
&[&context.payer],
|
||||||
|
context.last_blockhash,
|
||||||
|
);
|
||||||
|
try_recover_nested(
|
||||||
|
context,
|
||||||
|
program_id,
|
||||||
|
mint,
|
||||||
|
mint_authority,
|
||||||
|
nested_associated_token_address,
|
||||||
|
owner_associated_token_address,
|
||||||
|
wallet,
|
||||||
|
transaction,
|
||||||
|
Some(InstructionError::MissingRequiredSignature),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn fail_missing_wallet_signature_2022() {
|
||||||
|
let dummy_mint = Pubkey::new_unique();
|
||||||
|
let pt = program_test_2022(dummy_mint, true);
|
||||||
|
let mut context = pt.start_with_context().await;
|
||||||
|
check_missing_wallet_signature(&mut context, &spl_token_2022::id()).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn fail_missing_wallet_signature() {
|
||||||
|
let dummy_mint = Pubkey::new_unique();
|
||||||
|
let pt = program_test(dummy_mint, true);
|
||||||
|
let mut context = pt.start_with_context().await;
|
||||||
|
check_missing_wallet_signature(&mut context, &spl_token::id()).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_wrong_signer(context: &mut ProgramTestContext, program_id: &Pubkey) {
|
||||||
|
let wallet = Keypair::new();
|
||||||
|
let wrong_wallet = Keypair::new();
|
||||||
|
let (mint, mint_authority) = create_mint(context, program_id).await;
|
||||||
|
|
||||||
|
let owner_associated_token_address =
|
||||||
|
create_associated_token_account(context, &wallet.pubkey(), &mint, program_id).await;
|
||||||
|
let nested_associated_token_address = create_associated_token_account(
|
||||||
|
context,
|
||||||
|
&owner_associated_token_address,
|
||||||
|
&mint,
|
||||||
|
program_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let transaction = Transaction::new_signed_with_payer(
|
||||||
|
&[instruction::recover_nested(
|
||||||
|
&wrong_wallet.pubkey(),
|
||||||
|
&mint,
|
||||||
|
&mint,
|
||||||
|
program_id,
|
||||||
|
)],
|
||||||
|
Some(&context.payer.pubkey()),
|
||||||
|
&[&context.payer, &wrong_wallet],
|
||||||
|
context.last_blockhash,
|
||||||
|
);
|
||||||
|
try_recover_nested(
|
||||||
|
context,
|
||||||
|
program_id,
|
||||||
|
mint,
|
||||||
|
mint_authority,
|
||||||
|
nested_associated_token_address,
|
||||||
|
owner_associated_token_address,
|
||||||
|
wrong_wallet,
|
||||||
|
transaction,
|
||||||
|
Some(InstructionError::IllegalOwner),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn fail_wrong_signer_2022() {
|
||||||
|
let dummy_mint = Pubkey::new_unique();
|
||||||
|
let pt = program_test_2022(dummy_mint, true);
|
||||||
|
let mut context = pt.start_with_context().await;
|
||||||
|
check_wrong_signer(&mut context, &spl_token_2022::id()).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn fail_wrong_signer() {
|
||||||
|
let dummy_mint = Pubkey::new_unique();
|
||||||
|
let pt = program_test(dummy_mint, true);
|
||||||
|
let mut context = pt.start_with_context().await;
|
||||||
|
check_wrong_signer(&mut context, &spl_token::id()).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_not_nested(context: &mut ProgramTestContext, program_id: &Pubkey) {
|
||||||
|
let wallet = Keypair::new();
|
||||||
|
let wrong_wallet = Keypair::new();
|
||||||
|
let (mint, mint_authority) = create_mint(context, program_id).await;
|
||||||
|
|
||||||
|
let owner_associated_token_address =
|
||||||
|
create_associated_token_account(context, &wallet.pubkey(), &mint, program_id).await;
|
||||||
|
let nested_associated_token_address =
|
||||||
|
create_associated_token_account(context, &wrong_wallet.pubkey(), &mint, program_id).await;
|
||||||
|
|
||||||
|
let transaction = Transaction::new_signed_with_payer(
|
||||||
|
&[instruction::recover_nested(
|
||||||
|
&wallet.pubkey(),
|
||||||
|
&mint,
|
||||||
|
&mint,
|
||||||
|
program_id,
|
||||||
|
)],
|
||||||
|
Some(&context.payer.pubkey()),
|
||||||
|
&[&context.payer, &wallet],
|
||||||
|
context.last_blockhash,
|
||||||
|
);
|
||||||
|
try_recover_nested(
|
||||||
|
context,
|
||||||
|
program_id,
|
||||||
|
mint,
|
||||||
|
mint_authority,
|
||||||
|
nested_associated_token_address,
|
||||||
|
owner_associated_token_address,
|
||||||
|
wallet,
|
||||||
|
transaction,
|
||||||
|
Some(InstructionError::IllegalOwner),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn fail_not_nested_2022() {
|
||||||
|
let dummy_mint = Pubkey::new_unique();
|
||||||
|
let pt = program_test_2022(dummy_mint, true);
|
||||||
|
let mut context = pt.start_with_context().await;
|
||||||
|
check_not_nested(&mut context, &spl_token_2022::id()).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn fail_not_nested() {
|
||||||
|
let dummy_mint = Pubkey::new_unique();
|
||||||
|
let pt = program_test(dummy_mint, true);
|
||||||
|
let mut context = pt.start_with_context().await;
|
||||||
|
check_not_nested(&mut context, &spl_token::id()).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_wrong_address_derivation_owner(
|
||||||
|
context: &mut ProgramTestContext,
|
||||||
|
program_id: &Pubkey,
|
||||||
|
) {
|
||||||
|
let wallet = Keypair::new();
|
||||||
|
let wrong_wallet = Keypair::new();
|
||||||
|
let (mint, mint_authority) = create_mint(context, program_id).await;
|
||||||
|
|
||||||
|
let owner_associated_token_address =
|
||||||
|
create_associated_token_account(context, &wallet.pubkey(), &mint, program_id).await;
|
||||||
|
let nested_associated_token_address = create_associated_token_account(
|
||||||
|
context,
|
||||||
|
&owner_associated_token_address,
|
||||||
|
&mint,
|
||||||
|
program_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let wrong_owner_associated_token_address =
|
||||||
|
get_associated_token_address_with_program_id(&mint, &wrong_wallet.pubkey(), program_id);
|
||||||
|
let mut recover = instruction::recover_nested(&wallet.pubkey(), &mint, &mint, program_id);
|
||||||
|
recover.accounts[3] = AccountMeta::new(wrong_owner_associated_token_address, false);
|
||||||
|
let transaction = Transaction::new_signed_with_payer(
|
||||||
|
&[recover],
|
||||||
|
Some(&context.payer.pubkey()),
|
||||||
|
&[&context.payer, &wallet],
|
||||||
|
context.last_blockhash,
|
||||||
|
);
|
||||||
|
try_recover_nested(
|
||||||
|
context,
|
||||||
|
program_id,
|
||||||
|
mint,
|
||||||
|
mint_authority,
|
||||||
|
nested_associated_token_address,
|
||||||
|
wrong_owner_associated_token_address,
|
||||||
|
wallet,
|
||||||
|
transaction,
|
||||||
|
Some(InstructionError::InvalidSeeds),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn fail_wrong_address_derivation_owner_2022() {
|
||||||
|
let dummy_mint = Pubkey::new_unique();
|
||||||
|
let pt = program_test_2022(dummy_mint, true);
|
||||||
|
let mut context = pt.start_with_context().await;
|
||||||
|
check_wrong_address_derivation_owner(&mut context, &spl_token_2022::id()).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn fail_wrong_address_derivation_owner() {
|
||||||
|
let dummy_mint = Pubkey::new_unique();
|
||||||
|
let pt = program_test(dummy_mint, true);
|
||||||
|
let mut context = pt.start_with_context().await;
|
||||||
|
check_wrong_address_derivation_owner(&mut context, &spl_token::id()).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_owner_account_does_not_exist(context: &mut ProgramTestContext, program_id: &Pubkey) {
|
||||||
|
let wallet = Keypair::new();
|
||||||
|
let (mint, mint_authority) = create_mint(context, program_id).await;
|
||||||
|
|
||||||
|
let owner_associated_token_address =
|
||||||
|
get_associated_token_address_with_program_id(&wallet.pubkey(), &mint, program_id);
|
||||||
|
let nested_associated_token_address = create_associated_token_account(
|
||||||
|
context,
|
||||||
|
&owner_associated_token_address,
|
||||||
|
&mint,
|
||||||
|
program_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let transaction = Transaction::new_signed_with_payer(
|
||||||
|
&[instruction::recover_nested(
|
||||||
|
&wallet.pubkey(),
|
||||||
|
&mint,
|
||||||
|
&mint,
|
||||||
|
program_id,
|
||||||
|
)],
|
||||||
|
Some(&context.payer.pubkey()),
|
||||||
|
&[&context.payer, &wallet],
|
||||||
|
context.last_blockhash,
|
||||||
|
);
|
||||||
|
try_recover_nested(
|
||||||
|
context,
|
||||||
|
program_id,
|
||||||
|
mint,
|
||||||
|
mint_authority,
|
||||||
|
nested_associated_token_address,
|
||||||
|
owner_associated_token_address,
|
||||||
|
wallet,
|
||||||
|
transaction,
|
||||||
|
Some(InstructionError::IllegalOwner),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn fail_owner_account_does_not_exist() {
|
||||||
|
let dummy_mint = Pubkey::new_unique();
|
||||||
|
let pt = program_test_2022(dummy_mint, true);
|
||||||
|
let mut context = pt.start_with_context().await;
|
||||||
|
check_owner_account_does_not_exist(&mut context, &spl_token_2022::id()).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn fail_wrong_spl_token_program() {
|
||||||
|
let wallet = Keypair::new();
|
||||||
|
let dummy_mint = Pubkey::new_unique();
|
||||||
|
let pt = program_test_2022(dummy_mint, true);
|
||||||
|
let mut context = pt.start_with_context().await;
|
||||||
|
let program_id = spl_token_2022::id();
|
||||||
|
let wrong_program_id = spl_token::id();
|
||||||
|
let (mint, mint_authority) = create_mint(&mut context, &program_id).await;
|
||||||
|
|
||||||
|
let owner_associated_token_address =
|
||||||
|
create_associated_token_account(&mut context, &wallet.pubkey(), &mint, &program_id).await;
|
||||||
|
let nested_associated_token_address = create_associated_token_account(
|
||||||
|
&mut context,
|
||||||
|
&owner_associated_token_address,
|
||||||
|
&mint,
|
||||||
|
&program_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let transaction = Transaction::new_signed_with_payer(
|
||||||
|
&[instruction::recover_nested(
|
||||||
|
&wallet.pubkey(),
|
||||||
|
&mint,
|
||||||
|
&mint,
|
||||||
|
&wrong_program_id,
|
||||||
|
)],
|
||||||
|
Some(&context.payer.pubkey()),
|
||||||
|
&[&context.payer, &wallet],
|
||||||
|
context.last_blockhash,
|
||||||
|
);
|
||||||
|
try_recover_nested(
|
||||||
|
&mut context,
|
||||||
|
&program_id,
|
||||||
|
mint,
|
||||||
|
mint_authority,
|
||||||
|
nested_associated_token_address,
|
||||||
|
owner_associated_token_address,
|
||||||
|
wallet,
|
||||||
|
transaction,
|
||||||
|
Some(InstructionError::IllegalOwner),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn fail_destination_not_wallet_ata() {
|
||||||
|
let wallet = Keypair::new();
|
||||||
|
let wrong_wallet = Keypair::new();
|
||||||
|
let dummy_mint = Pubkey::new_unique();
|
||||||
|
let pt = program_test_2022(dummy_mint, true);
|
||||||
|
let program_id = spl_token_2022::id();
|
||||||
|
let mut context = pt.start_with_context().await;
|
||||||
|
let (mint, mint_authority) = create_mint(&mut context, &program_id).await;
|
||||||
|
|
||||||
|
let owner_associated_token_address =
|
||||||
|
create_associated_token_account(&mut context, &wallet.pubkey(), &mint, &program_id).await;
|
||||||
|
let nested_associated_token_address = create_associated_token_account(
|
||||||
|
&mut context,
|
||||||
|
&owner_associated_token_address,
|
||||||
|
&mint,
|
||||||
|
&program_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let wrong_destination_associated_token_account_address =
|
||||||
|
create_associated_token_account(&mut context, &wrong_wallet.pubkey(), &mint, &program_id)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut recover = instruction::recover_nested(&wallet.pubkey(), &mint, &mint, &program_id);
|
||||||
|
recover.accounts[2] =
|
||||||
|
AccountMeta::new(wrong_destination_associated_token_account_address, false);
|
||||||
|
|
||||||
|
let transaction = Transaction::new_signed_with_payer(
|
||||||
|
&[recover],
|
||||||
|
Some(&context.payer.pubkey()),
|
||||||
|
&[&context.payer, &wallet],
|
||||||
|
context.last_blockhash,
|
||||||
|
);
|
||||||
|
try_recover_nested(
|
||||||
|
&mut context,
|
||||||
|
&program_id,
|
||||||
|
mint,
|
||||||
|
mint_authority,
|
||||||
|
nested_associated_token_address,
|
||||||
|
owner_associated_token_address,
|
||||||
|
wallet,
|
||||||
|
transaction,
|
||||||
|
Some(InstructionError::InvalidSeeds),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
|
@ -1,13 +1,15 @@
|
||||||
// Mark this test as BPF-only due to current `ProgramTest` limitations when CPIing into the system program
|
// Mark this test as BPF-only due to current `ProgramTest` limitations when CPIing into the system program
|
||||||
#![cfg(feature = "test-bpf")]
|
#![cfg(feature = "test-bpf")]
|
||||||
|
|
||||||
|
mod program_test;
|
||||||
|
|
||||||
use {
|
use {
|
||||||
|
program_test::program_test,
|
||||||
solana_program::pubkey::Pubkey,
|
solana_program::pubkey::Pubkey,
|
||||||
solana_program_test::*,
|
solana_program_test::*,
|
||||||
solana_sdk::{program_pack::Pack, signature::Signer, transaction::Transaction},
|
solana_sdk::{program_pack::Pack, signature::Signer, transaction::Transaction},
|
||||||
spl_associated_token_account::{
|
spl_associated_token_account::{
|
||||||
get_associated_token_address, id, instruction::create_associated_token_account,
|
get_associated_token_address, instruction::create_associated_token_account,
|
||||||
processor::process_instruction,
|
|
||||||
},
|
},
|
||||||
spl_token::state::Account,
|
spl_token::state::Account,
|
||||||
};
|
};
|
||||||
|
@ -15,41 +17,6 @@ use {
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
use spl_associated_token_account::create_associated_token_account as deprecated_create_associated_token_account;
|
use spl_associated_token_account::create_associated_token_account as deprecated_create_associated_token_account;
|
||||||
|
|
||||||
fn program_test(token_mint_address: Pubkey, use_latest_spl_token: bool) -> ProgramTest {
|
|
||||||
let mut pc = ProgramTest::new(
|
|
||||||
"spl_associated_token_account",
|
|
||||||
id(),
|
|
||||||
processor!(process_instruction),
|
|
||||||
);
|
|
||||||
|
|
||||||
if use_latest_spl_token {
|
|
||||||
// TODO: Remove after Token >3.3.0 is available by default in program-test
|
|
||||||
pc.add_program(
|
|
||||||
"spl_token",
|
|
||||||
spl_token::id(),
|
|
||||||
processor!(spl_token::processor::Processor::process),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a token mint account
|
|
||||||
//
|
|
||||||
// The account data was generated by running:
|
|
||||||
// $ solana account EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v \
|
|
||||||
// --output-file tests/fixtures/token-mint-data.bin
|
|
||||||
//
|
|
||||||
pc.add_account_with_file_data(
|
|
||||||
token_mint_address,
|
|
||||||
1461600,
|
|
||||||
spl_token::id(),
|
|
||||||
"token-mint-data.bin",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Dial down the BPF compute budget to detect if the program gets bloated in the future
|
|
||||||
pc.set_compute_max_units(50_000);
|
|
||||||
|
|
||||||
pc
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn success_create() {
|
async fn success_create() {
|
||||||
let wallet_address = Pubkey::new_unique();
|
let wallet_address = Pubkey::new_unique();
|
||||||
|
|
Loading…
Reference in New Issue