Skip to content
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
29 changes: 29 additions & 0 deletions clients/cli/src/clap_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ pub enum CommandName {
AccountInfo,
MultisigInfo,
Display,
RecoverNested,
Gc,
SyncNative,
EnableRequiredTransferMemos,
Expand Down Expand Up @@ -2127,6 +2128,34 @@ pub fn app<'a>(
.help("The address of the SPL Token mint, account, or multisig to query"),
),
)
.subcommand(
SubCommand::with_name(CommandName::RecoverNested.into())
.about("Recover tokens from a nested associated token account")
.arg(
Arg::with_name("owner_token_mint")
.validator(|s| is_valid_pubkey(s))
.value_name("OWNER_TOKEN_MINT_ADDRESS")
.takes_value(true)
.index(1)
.required(true)
.help("Mint of the associated token account that owns the nested account"),
)
.arg(
Arg::with_name("nested_token_mint")
.validator(|s| is_valid_pubkey(s))
.value_name("NESTED_TOKEN_MINT_ADDRESS")
.takes_value(true)
.index(2)
.required(true)
.help(
"Mint of the tokens held in the nested associated token account; may \
be the same as OWNER_TOKEN_MINT_ADDRESS",
),
)
.arg(owner_keypair_arg())
.nonce_args(true)
.offline_args(),
)
.subcommand(
SubCommand::with_name(CommandName::Gc.into())
.about("Cleanup unnecessary token accounts")
Expand Down
77 changes: 76 additions & 1 deletion clients/cli/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ use {
elgamal::{self, ElGamalKeypair},
},
solana_zk_sdk_pod::encryption::elgamal::PodElGamalPubkey,
spl_associated_token_account_interface::address::get_associated_token_address_with_program_id,
spl_associated_token_account_interface::{
address::get_associated_token_address_with_program_id, instruction::recover_nested,
},
spl_token_2022_interface::{
extension::{
confidential_mint_burn::ConfidentialMintBurn,
Expand Down Expand Up @@ -3387,6 +3389,57 @@ async fn command_gc(
Ok(results.join(""))
}

async fn command_recover_nested(
config: &Config<'_>,
owner: Pubkey,
owner_token_mint: Pubkey,
nested_token_mint: Pubkey,
bulk_signers: BulkSigners,
) -> CommandResult {
let owner_associated_account =
get_associated_token_address_with_program_id(&owner, &owner_token_mint, &config.program_id);
let nested_associated_account = get_associated_token_address_with_program_id(
&owner_associated_account,
&nested_token_mint,
&config.program_id,
);
let destination_associated_account = get_associated_token_address_with_program_id(
&owner,
&nested_token_mint,
&config.program_id,
);

println_display(
config,
format!(
"Recovering nested associated token account\n Owner: {}\n Owner associated account: {}\n Nested associated account: {}\n Destination associated account: {}",
owner,
owner_associated_account,
nested_associated_account,
destination_associated_account,
),
);

let instruction = recover_nested(
&owner,
&owner_token_mint,
&nested_token_mint,
&config.program_id,
);
let token = token_client_from_config(config, &nested_token_mint, None)?;
let res = token.process_ixs(&[instruction], &bulk_signers).await?;
let tx_return = finish_tx(config, &res, false).await?;

Ok(match tx_return {
TransactionReturnData::CliSignature(signature) => {
config.output_format.formatted_string(&signature)
}
TransactionReturnData::CliSignOnlyData(sign_only_data) => {
config.output_format.formatted_string(&sign_only_data)
}
})
}

async fn command_sync_native(config: &Config<'_>, native_account_address: Pubkey) -> CommandResult {
let token = native_token_client_from_config(config)?;

Expand Down Expand Up @@ -5284,6 +5337,28 @@ pub async fn process_command(
.unwrap();
command_display(config, address).await
}
(CommandName::RecoverNested, arg_matches) => {
let owner_token_mint =
pubkey_of_signer(arg_matches, "owner_token_mint", &mut wallet_manager)
.unwrap()
.unwrap();
let nested_token_mint =
pubkey_of_signer(arg_matches, "nested_token_mint", &mut wallet_manager)
.unwrap()
.unwrap();
let (owner_signer, owner) =
config.signer_or_default(arg_matches, "owner", &mut wallet_manager);
push_signer_with_dedup(owner_signer, &mut bulk_signers);

command_recover_nested(
config,
owner,
owner_token_mint,
nested_token_mint,
bulk_signers,
)
.await
}
(CommandName::Gc, arg_matches) => {
match config.output_format {
OutputFormat::Json | OutputFormat::JsonCompact => {
Expand Down
133 changes: 133 additions & 0 deletions clients/cli/tests/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ async fn main() {
async_trial!(multisig_pause, test_validator, payer),
async_trial!(permissioned_burn, test_validator, payer),
async_trial!(confidential_mint_burn, test_validator, payer),
async_trial!(recover_nested, test_validator, payer),
// GC messes with every other test, so have it on its own test validator
async_trial!(gc, gc_test_validator, gc_payer),
];
Expand Down Expand Up @@ -1575,6 +1576,138 @@ async fn disable_mint_authority(test_validator: &TestValidator, payer: &Keypair)
}
}

async fn recover_nested(test_validator: &TestValidator, payer: &Keypair) {
for program_id in VALID_TOKEN_PROGRAM_IDS.iter() {
for same_mint in [false, true] {
let config = test_config_with_default_signer(test_validator, payer, program_id);
let owner = Keypair::new();
let owner_keypair_file = NamedTempFile::new().unwrap();
write_keypair_file(&owner, &owner_keypair_file).unwrap();
let fee_payer_keypair_file = NamedTempFile::new().unwrap();
write_keypair_file(payer, &fee_payer_keypair_file).unwrap();
let blockhash = config.rpc_client.get_latest_blockhash().await.unwrap();
let transaction = Transaction::new_signed_with_payer(
&[system_instruction::transfer(
&payer.pubkey(),
&owner.pubkey(),
1_000_000,
)],
Some(&payer.pubkey()),
&[payer],
blockhash,
);
config
.rpc_client
.send_and_confirm_transaction(&transaction)
.await
.unwrap();

let owner_token_mint = create_token(&config, payer).await;
let nested_token_mint = if same_mint {
owner_token_mint
} else {
create_token(&config, payer).await
};
let owner_associated_account =
create_associated_account(&config, payer, &owner_token_mint, &owner.pubkey()).await;
let destination_associated_account = if same_mint {
owner_associated_account
} else {
create_associated_account(&config, payer, &nested_token_mint, &owner.pubkey()).await
};
let nested_associated_account = create_associated_account(
&config,
payer,
&nested_token_mint,
&owner_associated_account,
)
.await;

mint_tokens(
&config,
payer,
nested_token_mint,
1.0,
nested_associated_account,
)
.await
.unwrap();
let nested_account_lamports = config
.rpc_client
.get_account(&nested_associated_account)
.await
.unwrap()
.lamports;
for token_account in [
owner_associated_account,
destination_associated_account,
nested_associated_account,
] {
assert_eq!(
config
.rpc_client
.get_account(&token_account)
.await
.unwrap()
.owner,
*program_id,
"token account {token_account} has the wrong program owner"
);
}
let owner_lamports_before = config
.rpc_client
.get_balance(&owner.pubkey())
.await
.unwrap();
exec_test_cmd(
&config,
&[
"spl-token",
CommandName::RecoverNested.into(),
&owner_token_mint.to_string(),
&nested_token_mint.to_string(),
"--owner",
owner_keypair_file.path().to_str().unwrap(),
"--fee-payer",
fee_payer_keypair_file.path().to_str().unwrap(),
"--program-id",
&program_id.to_string(),
],
)
.await
.unwrap_or_else(|error| {
panic!(
"recover-nested failed for program {program_id} with same_mint={same_mint}: \
{error}"
)
});

let destination = config
.rpc_client
.get_token_account(&destination_associated_account)
.await
.unwrap()
.unwrap();
let expected_amount = spl_token_2022::ui_amount_to_amount(1.0, TEST_DECIMALS);
assert_eq!(destination.token_amount.amount, expected_amount.to_string());
config
.rpc_client
.get_account(&nested_associated_account)
.await
.unwrap_err();
let owner_lamports_after = config
.rpc_client
.get_balance(&owner.pubkey())
.await
.unwrap();
assert_eq!(
owner_lamports_after,
owner_lamports_before + nested_account_lamports
);
}
}
}

async fn gc(test_validator: &TestValidator, payer: &Keypair) {
for program_id in VALID_TOKEN_PROGRAM_IDS.iter() {
let mut config = test_config_with_default_signer(test_validator, payer, program_id);
Expand Down