diff --git a/token/cli/src/bench.rs b/token/cli/src/bench.rs index 27cf0a4482c..644cdc55333 100644 --- a/token/cli/src/bench.rs +++ b/token/cli/src/bench.rs @@ -1,19 +1,16 @@ /// The `bench` subcommand use { - crate::{config::Config, owner_address_arg, CommandResult, Error}, - clap::{value_t_or_exit, App, AppSettings, Arg, ArgMatches, SubCommand}, - solana_clap_utils::{ - input_parsers::pubkey_of_signer, - input_validators::{is_amount, is_parsable, is_valid_pubkey}, - }, + crate::{clap_app::Error, command::CommandResult, config::Config}, + clap::{value_t_or_exit, ArgMatches}, + solana_clap_utils::input_parsers::pubkey_of_signer, solana_client::{ nonblocking::rpc_client::RpcClient, rpc_client::RpcClient as BlockingRpcClient, tpu_client::TpuClient, tpu_client::TpuClientConfig, }, solana_remote_wallet::remote_wallet::RemoteWalletManager, solana_sdk::{ - message::Message, native_token::Sol, program_pack::Pack, pubkey::Pubkey, signature::Signer, - system_instruction, + message::Message, native_token::lamports_to_sol, native_token::Sol, program_pack::Pack, + pubkey::Pubkey, signature::Signer, system_instruction, }, spl_associated_token_account::*, spl_token_2022::{ @@ -24,147 +21,6 @@ use { std::{rc::Rc, sync::Arc, time::Instant}, }; -pub(crate) trait BenchSubCommand { - fn bench_subcommand(self) -> Self; -} - -impl BenchSubCommand for App<'_, '_> { - fn bench_subcommand(self) -> Self { - self.subcommand( - SubCommand::with_name("bench") - .about("Token benchmarking facilities") - .setting(AppSettings::InferSubcommands) - .setting(AppSettings::SubcommandRequiredElseHelp) - .subcommand( - SubCommand::with_name("create-accounts") - .about("Create multiple token accounts for benchmarking") - .arg( - Arg::with_name("token") - .validator(is_valid_pubkey) - .value_name("TOKEN_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The token that the accounts will hold"), - ) - .arg( - Arg::with_name("n") - .validator(is_parsable::) - .value_name("N") - .takes_value(true) - .index(2) - .required(true) - .help("The number of accounts to create"), - ) - .arg(owner_address_arg()), - ) - .subcommand( - SubCommand::with_name("close-accounts") - .about("Close multiple token accounts used for benchmarking") - .arg( - Arg::with_name("token") - .validator(is_valid_pubkey) - .value_name("TOKEN_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The token that the accounts held"), - ) - .arg( - Arg::with_name("n") - .validator(is_parsable::) - .value_name("N") - .takes_value(true) - .index(2) - .required(true) - .help("The number of accounts to close"), - ) - .arg(owner_address_arg()), - ) - .subcommand( - SubCommand::with_name("deposit-into") - .about("Deposit tokens into multiple accounts") - .arg( - Arg::with_name("token") - .validator(is_valid_pubkey) - .value_name("TOKEN_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The token that the accounts will hold"), - ) - .arg( - Arg::with_name("n") - .validator(is_parsable::) - .value_name("N") - .takes_value(true) - .index(2) - .required(true) - .help("The number of accounts to deposit into"), - ) - .arg( - Arg::with_name("amount") - .validator(is_amount) - .value_name("TOKEN_AMOUNT") - .takes_value(true) - .index(3) - .required(true) - .help("Amount to deposit into each account, in tokens"), - ) - .arg( - Arg::with_name("from") - .long("from") - .validator(is_valid_pubkey) - .value_name("SOURCE_TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .help("The source token account address [default: associated token account for --owner]") - ) - .arg(owner_address_arg()), - ) - .subcommand( - SubCommand::with_name("withdraw-from") - .about("Withdraw tokens from multiple accounts") - .arg( - Arg::with_name("token") - .validator(is_valid_pubkey) - .value_name("TOKEN_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The token that the accounts hold"), - ) - .arg( - Arg::with_name("n") - .validator(is_parsable::) - .value_name("N") - .takes_value(true) - .index(2) - .required(true) - .help("The number of accounts to withdraw from"), - ) - .arg( - Arg::with_name("amount") - .validator(is_amount) - .value_name("TOKEN_AMOUNT") - .takes_value(true) - .index(3) - .required(true) - .help("Amount to withdraw from each account, in tokens"), - ) - .arg( - Arg::with_name("to") - .long("to") - .validator(is_valid_pubkey) - .value_name("RECIPIENT_TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .help("The recipient token account address [default: associated token account for --owner]") - ) - .arg(owner_address_arg()), - ), - ) - } -} - pub(crate) async fn bench_process_command( matches: &ArgMatches<'_>, config: &Config<'_>, @@ -287,7 +143,7 @@ async fn command_create_accounts( .get_minimum_balance_for_rent_exemption(Account::get_packed_len()) .await?; - let mut lamports_required = 0; + let mut lamports_required: u64 = 0; let token_addresses_with_seed = get_token_addresses_with_seed(&program_id, token, owner, n); let mut messages = vec![]; @@ -298,7 +154,8 @@ async fn command_create_accounts( for (account, (address, seed)) in accounts_chunk.iter().zip(address_chunk) { if account.is_none() { - lamports_required += minimum_balance_for_rent_exemption; + lamports_required = + lamports_required.saturating_add(minimum_balance_for_rent_exemption); messages.push(Message::new( &[ system_instruction::create_account_with_seed( @@ -440,8 +297,13 @@ async fn send_messages( let blockhash = config.rpc_client.get_latest_blockhash().await?; let mut message = messages[0].clone(); message.recent_blockhash = blockhash; - lamports_required += - config.rpc_client.get_fee_for_message(&message).await? * messages.len() as u64; + lamports_required = lamports_required.saturating_add( + config + .rpc_client + .get_fee_for_message(&message) + .await? + .saturating_mul(messages.len() as u64), + ); println!( "Sending {:?} messages for ~{}", @@ -449,7 +311,7 @@ async fn send_messages( Sol(lamports_required) ); - crate::check_fee_payer_balance(config, lamports_required).await?; + check_fee_payer_balance(config, lamports_required).await?; // TODO use async tpu client once it's available in 1.11 let start = Instant::now(); @@ -489,3 +351,21 @@ async fn send_messages( Ok(()) } + +async fn check_fee_payer_balance(config: &Config<'_>, required_balance: u64) -> Result<(), Error> { + let balance = config + .rpc_client + .get_balance(&config.fee_payer()?.pubkey()) + .await?; + if balance < required_balance { + Err(format!( + "Fee payer, {}, has insufficient balance: {} required, {} available", + config.fee_payer()?.pubkey(), + lamports_to_sol(required_balance), + lamports_to_sol(balance) + ) + .into()) + } else { + Ok(()) + } +} diff --git a/token/cli/src/clap_app.rs b/token/cli/src/clap_app.rs new file mode 100644 index 00000000000..8cc108b8f78 --- /dev/null +++ b/token/cli/src/clap_app.rs @@ -0,0 +1,2326 @@ +use { + clap::{ + crate_description, crate_name, crate_version, App, AppSettings, Arg, ArgGroup, SubCommand, + }, + solana_clap_utils::{ + fee_payer::fee_payer_arg, + input_validators::{ + is_amount, is_amount_or_all, is_parsable, is_pubkey, is_url_or_moniker, + is_valid_pubkey, is_valid_signer, + }, + memo::memo_arg, + nonce::*, + offline::{self, *}, + ArgConstant, + }, + solana_sdk::{instruction::AccountMeta, pubkey::Pubkey}, + spl_token_2022::instruction::{AuthorityType, MAX_SIGNERS, MIN_SIGNERS}, + std::{fmt, str::FromStr}, + strum::IntoEnumIterator, + strum_macros::{EnumIter, EnumString, IntoStaticStr}, +}; + +pub type Error = Box; + +pub const OWNER_ADDRESS_ARG: ArgConstant<'static> = ArgConstant { + name: "owner", + long: "owner", + help: "Address of the primary authority controlling a mint or account. Defaults to the client keypair address.", +}; + +pub const OWNER_KEYPAIR_ARG: ArgConstant<'static> = ArgConstant { + name: "owner", + long: "owner", + help: "Keypair of the primary authority controlling a mint or account. Defaults to the client keypair.", +}; + +pub const MINT_ADDRESS_ARG: ArgConstant<'static> = ArgConstant { + name: "mint_address", + long: "mint-address", + help: "Address of mint that token account is associated with. Required by --sign-only", +}; + +pub const MINT_DECIMALS_ARG: ArgConstant<'static> = ArgConstant { + name: "mint_decimals", + long: "mint-decimals", + help: "Decimals of mint that token account is associated with. Required by --sign-only", +}; + +pub const DELEGATE_ADDRESS_ARG: ArgConstant<'static> = ArgConstant { + name: "delegate_address", + long: "delegate-address", + help: "Address of delegate currently assigned to token account. Required by --sign-only", +}; + +pub const TRANSFER_LAMPORTS_ARG: ArgConstant<'static> = ArgConstant { + name: "transfer_lamports", + long: "transfer-lamports", + help: "Additional lamports to transfer to make account rent-exempt after reallocation. Required by --sign-only", +}; + +pub const MULTISIG_SIGNER_ARG: ArgConstant<'static> = ArgConstant { + name: "multisig_signer", + long: "multisig-signer", + help: "Member signer of a multisig account", +}; + +pub static VALID_TOKEN_PROGRAM_IDS: [Pubkey; 2] = [spl_token_2022::ID, spl_token::ID]; + +#[derive(Debug, Clone, Copy, PartialEq, EnumString, IntoStaticStr)] +#[strum(serialize_all = "kebab-case")] +pub enum CommandName { + CreateToken, + Close, + CloseMint, + Bench, + CreateAccount, + CreateMultisig, + Authorize, + SetInterestRate, + Transfer, + Burn, + Mint, + Freeze, + Thaw, + Wrap, + Unwrap, + Approve, + Revoke, + Balance, + Supply, + Accounts, + Address, + AccountInfo, + MultisigInfo, + Display, + Gc, + SyncNative, + EnableRequiredTransferMemos, + DisableRequiredTransferMemos, + EnableCpiGuard, + DisableCpiGuard, + UpdateDefaultAccountState, + UpdateMetadataAddress, + WithdrawWithheldTokens, + SetTransferFee, + WithdrawExcessLamports, + SetTransferHook, + InitializeMetadata, + UpdateMetadata, + UpdateConfidentialTransferSettings, + ConfigureConfidentialTransferAccount, + EnableConfidentialCredits, + DisableConfidentialCredits, + EnableNonConfidentialCredits, + DisableNonConfidentialCredits, + DepositConfidentialTokens, + WithdrawConfidentialTokens, + ApplyPendingBalance, +} +impl fmt::Display for CommandName { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } +} +#[derive(Debug, Clone, Copy, PartialEq, EnumString, IntoStaticStr)] +#[strum(serialize_all = "kebab-case")] +pub enum AccountMetaRole { + Readonly, + Writable, + ReadonlySigner, + WritableSigner, +} +impl fmt::Display for AccountMetaRole { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } +} +pub fn parse_transfer_hook_account(string: T) -> Result +where + T: AsRef + fmt::Display, +{ + match string.as_ref().split(':').collect::>().as_slice() { + [address, role] => { + let address = Pubkey::from_str(address).map_err(|e| format!("{e}"))?; + let meta = match AccountMetaRole::from_str(role).map_err(|e| format!("{e}"))? { + AccountMetaRole::Readonly => AccountMeta::new_readonly(address, false), + AccountMetaRole::Writable => AccountMeta::new(address, false), + AccountMetaRole::ReadonlySigner => AccountMeta::new_readonly(address, true), + AccountMetaRole::WritableSigner => AccountMeta::new(address, true), + }; + Ok(meta) + } + _ => Err("Transfer hook account must be present as
:".to_string()), + } +} +fn validate_transfer_hook_account(string: T) -> Result<(), String> +where + T: AsRef + fmt::Display, +{ + match string.as_ref().split(':').collect::>().as_slice() { + [address, role] => { + is_valid_pubkey(address)?; + AccountMetaRole::from_str(role) + .map(|_| ()) + .map_err(|e| format!("{e}")) + } + _ => Err("Transfer hook account must be present as
:".to_string()), + } +} +#[derive(Debug, Clone, PartialEq, EnumIter, EnumString, IntoStaticStr)] +#[strum(serialize_all = "kebab-case")] +pub enum CliAuthorityType { + Mint, + Freeze, + Owner, + Close, + CloseMint, + TransferFeeConfig, + WithheldWithdraw, + InterestRate, + PermanentDelegate, + ConfidentialTransferMint, + TransferHookProgramId, + ConfidentialTransferFee, + MetadataPointer, + Metadata, +} +impl TryFrom for AuthorityType { + type Error = Error; + fn try_from(authority_type: CliAuthorityType) -> Result { + match authority_type { + CliAuthorityType::Mint => Ok(AuthorityType::MintTokens), + CliAuthorityType::Freeze => Ok(AuthorityType::FreezeAccount), + CliAuthorityType::Owner => Ok(AuthorityType::AccountOwner), + CliAuthorityType::Close => Ok(AuthorityType::CloseAccount), + CliAuthorityType::CloseMint => Ok(AuthorityType::CloseMint), + CliAuthorityType::TransferFeeConfig => Ok(AuthorityType::TransferFeeConfig), + CliAuthorityType::WithheldWithdraw => Ok(AuthorityType::WithheldWithdraw), + CliAuthorityType::InterestRate => Ok(AuthorityType::InterestRate), + CliAuthorityType::PermanentDelegate => Ok(AuthorityType::PermanentDelegate), + CliAuthorityType::ConfidentialTransferMint => { + Ok(AuthorityType::ConfidentialTransferMint) + } + CliAuthorityType::TransferHookProgramId => Ok(AuthorityType::TransferHookProgramId), + CliAuthorityType::ConfidentialTransferFee => { + Ok(AuthorityType::ConfidentialTransferFeeConfig) + } + CliAuthorityType::MetadataPointer => Ok(AuthorityType::MetadataPointer), + CliAuthorityType::Metadata => { + Err("Metadata authority does not map to a token authority type".into()) + } + } + } +} + +pub fn owner_address_arg<'a, 'b>() -> Arg<'a, 'b> { + Arg::with_name(OWNER_ADDRESS_ARG.name) + .long(OWNER_ADDRESS_ARG.long) + .takes_value(true) + .value_name("OWNER_ADDRESS") + .validator(is_valid_pubkey) + .help(OWNER_ADDRESS_ARG.help) +} + +pub fn owner_keypair_arg_with_value_name<'a, 'b>(value_name: &'static str) -> Arg<'a, 'b> { + Arg::with_name(OWNER_KEYPAIR_ARG.name) + .long(OWNER_KEYPAIR_ARG.long) + .takes_value(true) + .value_name(value_name) + .validator(is_valid_signer) + .help(OWNER_KEYPAIR_ARG.help) +} + +pub fn owner_keypair_arg<'a, 'b>() -> Arg<'a, 'b> { + owner_keypair_arg_with_value_name("OWNER_KEYPAIR") +} + +pub fn mint_address_arg<'a, 'b>() -> Arg<'a, 'b> { + Arg::with_name(MINT_ADDRESS_ARG.name) + .long(MINT_ADDRESS_ARG.long) + .takes_value(true) + .value_name("MINT_ADDRESS") + .validator(is_valid_pubkey) + .help(MINT_ADDRESS_ARG.help) +} + +fn is_mint_decimals(string: String) -> Result<(), String> { + is_parsable::(string) +} + +pub fn mint_decimals_arg<'a, 'b>() -> Arg<'a, 'b> { + Arg::with_name(MINT_DECIMALS_ARG.name) + .long(MINT_DECIMALS_ARG.long) + .takes_value(true) + .value_name("MINT_DECIMALS") + .validator(is_mint_decimals) + .help(MINT_DECIMALS_ARG.help) +} + +pub trait MintArgs { + fn mint_args(self) -> Self; +} + +impl MintArgs for App<'_, '_> { + fn mint_args(self) -> Self { + self.arg(mint_address_arg().requires(MINT_DECIMALS_ARG.name)) + .arg(mint_decimals_arg().requires(MINT_ADDRESS_ARG.name)) + } +} + +pub fn delegate_address_arg<'a, 'b>() -> Arg<'a, 'b> { + Arg::with_name(DELEGATE_ADDRESS_ARG.name) + .long(DELEGATE_ADDRESS_ARG.long) + .takes_value(true) + .value_name("DELEGATE_ADDRESS") + .validator(is_valid_pubkey) + .help(DELEGATE_ADDRESS_ARG.help) +} + +pub fn transfer_lamports_arg<'a, 'b>() -> Arg<'a, 'b> { + Arg::with_name(TRANSFER_LAMPORTS_ARG.name) + .long(TRANSFER_LAMPORTS_ARG.long) + .takes_value(true) + .value_name("LAMPORTS") + .validator(is_amount) + .help(TRANSFER_LAMPORTS_ARG.help) +} + +pub fn multisig_signer_arg<'a, 'b>() -> Arg<'a, 'b> { + Arg::with_name(MULTISIG_SIGNER_ARG.name) + .long(MULTISIG_SIGNER_ARG.long) + .validator(is_valid_signer) + .value_name("MULTISIG_SIGNER") + .takes_value(true) + .multiple(true) + .min_values(0u64) + .max_values(MAX_SIGNERS as u64) + .help(MULTISIG_SIGNER_ARG.help) +} + +fn is_multisig_minimum_signers(string: String) -> Result<(), String> { + let v = u8::from_str(&string).map_err(|e| e.to_string())? as usize; + if v < MIN_SIGNERS { + Err(format!("must be at least {}", MIN_SIGNERS)) + } else if v > MAX_SIGNERS { + Err(format!("must be at most {}", MAX_SIGNERS)) + } else { + Ok(()) + } +} + +fn is_valid_token_program_id(string: T) -> Result<(), String> +where + T: AsRef + fmt::Display, +{ + match is_pubkey(string.as_ref()) { + Ok(()) => { + let program_id = string.as_ref().parse::().unwrap(); + if VALID_TOKEN_PROGRAM_IDS.contains(&program_id) { + Ok(()) + } else { + Err(format!("Unrecognized token program id: {}", program_id)) + } + } + Err(e) => Err(e), + } +} +struct SignOnlyNeedsFullMintSpec {} +impl offline::ArgsConfig for SignOnlyNeedsFullMintSpec { + fn sign_only_arg<'a, 'b>(&self, arg: Arg<'a, 'b>) -> Arg<'a, 'b> { + arg.requires_all(&[MINT_ADDRESS_ARG.name, MINT_DECIMALS_ARG.name]) + } + fn signer_arg<'a, 'b>(&self, arg: Arg<'a, 'b>) -> Arg<'a, 'b> { + arg.requires_all(&[MINT_ADDRESS_ARG.name, MINT_DECIMALS_ARG.name]) + } +} + +struct SignOnlyNeedsMintDecimals {} +impl offline::ArgsConfig for SignOnlyNeedsMintDecimals { + fn sign_only_arg<'a, 'b>(&self, arg: Arg<'a, 'b>) -> Arg<'a, 'b> { + arg.requires_all(&[MINT_DECIMALS_ARG.name]) + } + fn signer_arg<'a, 'b>(&self, arg: Arg<'a, 'b>) -> Arg<'a, 'b> { + arg.requires_all(&[MINT_DECIMALS_ARG.name]) + } +} + +struct SignOnlyNeedsMintAddress {} +impl offline::ArgsConfig for SignOnlyNeedsMintAddress { + fn sign_only_arg<'a, 'b>(&self, arg: Arg<'a, 'b>) -> Arg<'a, 'b> { + arg.requires_all(&[MINT_ADDRESS_ARG.name]) + } + fn signer_arg<'a, 'b>(&self, arg: Arg<'a, 'b>) -> Arg<'a, 'b> { + arg.requires_all(&[MINT_ADDRESS_ARG.name]) + } +} + +struct SignOnlyNeedsDelegateAddress {} +impl offline::ArgsConfig for SignOnlyNeedsDelegateAddress { + fn sign_only_arg<'a, 'b>(&self, arg: Arg<'a, 'b>) -> Arg<'a, 'b> { + arg.requires_all(&[DELEGATE_ADDRESS_ARG.name]) + } + fn signer_arg<'a, 'b>(&self, arg: Arg<'a, 'b>) -> Arg<'a, 'b> { + arg.requires_all(&[DELEGATE_ADDRESS_ARG.name]) + } +} + +struct SignOnlyNeedsTransferLamports {} +impl offline::ArgsConfig for SignOnlyNeedsTransferLamports { + fn sign_only_arg<'a, 'b>(&self, arg: Arg<'a, 'b>) -> Arg<'a, 'b> { + arg.requires_all(&[TRANSFER_LAMPORTS_ARG.name]) + } + fn signer_arg<'a, 'b>(&self, arg: Arg<'a, 'b>) -> Arg<'a, 'b> { + arg.requires_all(&[TRANSFER_LAMPORTS_ARG.name]) + } +} + +pub fn minimum_signers_help_string() -> String { + format!( + "The minimum number of signers required to allow the operation. [{} <= M <= N]", + MIN_SIGNERS + ) +} + +pub fn multisig_member_help_string() -> String { + format!( + "The public keys for each of the N signing members of this account. [{} <= N <= {}]", + MIN_SIGNERS, MAX_SIGNERS + ) +} + +pub(crate) trait BenchSubCommand { + fn bench_subcommand(self) -> Self; +} + +impl BenchSubCommand for App<'_, '_> { + fn bench_subcommand(self) -> Self { + self.subcommand( + SubCommand::with_name("bench") + .about("Token benchmarking facilities") + .setting(AppSettings::InferSubcommands) + .setting(AppSettings::SubcommandRequiredElseHelp) + .subcommand( + SubCommand::with_name("create-accounts") + .about("Create multiple token accounts for benchmarking") + .arg( + Arg::with_name("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("The token that the accounts will hold"), + ) + .arg( + Arg::with_name("n") + .validator(is_parsable::) + .value_name("N") + .takes_value(true) + .index(2) + .required(true) + .help("The number of accounts to create"), + ) + .arg(owner_address_arg()), + ) + .subcommand( + SubCommand::with_name("close-accounts") + .about("Close multiple token accounts used for benchmarking") + .arg( + Arg::with_name("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("The token that the accounts held"), + ) + .arg( + Arg::with_name("n") + .validator(is_parsable::) + .value_name("N") + .takes_value(true) + .index(2) + .required(true) + .help("The number of accounts to close"), + ) + .arg(owner_address_arg()), + ) + .subcommand( + SubCommand::with_name("deposit-into") + .about("Deposit tokens into multiple accounts") + .arg( + Arg::with_name("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("The token that the accounts will hold"), + ) + .arg( + Arg::with_name("n") + .validator(is_parsable::) + .value_name("N") + .takes_value(true) + .index(2) + .required(true) + .help("The number of accounts to deposit into"), + ) + .arg( + Arg::with_name("amount") + .validator(is_amount) + .value_name("TOKEN_AMOUNT") + .takes_value(true) + .index(3) + .required(true) + .help("Amount to deposit into each account, in tokens"), + ) + .arg( + Arg::with_name("from") + .long("from") + .validator(is_valid_pubkey) + .value_name("SOURCE_TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .help("The source token account address [default: associated token account for --owner]") + ) + .arg(owner_address_arg()), + ) + .subcommand( + SubCommand::with_name("withdraw-from") + .about("Withdraw tokens from multiple accounts") + .arg( + Arg::with_name("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("The token that the accounts hold"), + ) + .arg( + Arg::with_name("n") + .validator(is_parsable::) + .value_name("N") + .takes_value(true) + .index(2) + .required(true) + .help("The number of accounts to withdraw from"), + ) + .arg( + Arg::with_name("amount") + .validator(is_amount) + .value_name("TOKEN_AMOUNT") + .takes_value(true) + .index(3) + .required(true) + .help("Amount to withdraw from each account, in tokens"), + ) + .arg( + Arg::with_name("to") + .long("to") + .validator(is_valid_pubkey) + .value_name("RECIPIENT_TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .help("The recipient token account address [default: associated token account for --owner]") + ) + .arg(owner_address_arg()), + ), + ) + } +} + +pub fn app<'a, 'b>( + default_decimals: &'a str, + minimum_signers_help: &'b str, + multisig_member_help: &'b str, +) -> App<'a, 'b> { + App::new(crate_name!()) + .about(crate_description!()) + .version(crate_version!()) + .setting(AppSettings::SubcommandRequiredElseHelp) + .arg( + Arg::with_name("config_file") + .short("C") + .long("config") + .value_name("PATH") + .takes_value(true) + .global(true) + .help("Configuration file to use"), + ) + .arg( + Arg::with_name("verbose") + .short("v") + .long("verbose") + .takes_value(false) + .global(true) + .help("Show additional information"), + ) + .arg( + Arg::with_name("output_format") + .long("output") + .value_name("FORMAT") + .global(true) + .takes_value(true) + .possible_values(&["json", "json-compact"]) + .help("Return information in specified output format"), + ) + .arg( + Arg::with_name("program_id") + .short("p") + .long("program-id") + .value_name("ADDRESS") + .takes_value(true) + .global(true) + .validator(is_valid_token_program_id) + .help("SPL Token program id"), + ) + .arg( + Arg::with_name("json_rpc_url") + .short("u") + .long("url") + .value_name("URL_OR_MONIKER") + .takes_value(true) + .global(true) + .validator(is_url_or_moniker) + .help( + "URL for Solana's JSON RPC or moniker (or their first letter): \ + [mainnet-beta, testnet, devnet, localhost] \ + Default from the configuration file." + ), + ) + .arg(fee_payer_arg().global(true)) + .arg( + Arg::with_name("use_unchecked_instruction") + .long("use-unchecked-instruction") + .takes_value(false) + .global(true) + .hidden(true) + .help("Use unchecked instruction if appropriate. Supports transfer, burn, mint, and approve."), + ) + .bench_subcommand() + .subcommand(SubCommand::with_name(CommandName::CreateToken.into()).about("Create a new token") + .arg( + Arg::with_name("token_keypair") + .value_name("TOKEN_KEYPAIR") + .validator(is_valid_signer) + .takes_value(true) + .index(1) + .help( + "Specify the token keypair. \ + This may be a keypair file or the ASK keyword. \ + [default: randomly generated keypair]" + ), + ) + .arg( + Arg::with_name("mint_authority") + .long("mint-authority") + .alias("owner") + .value_name("ADDRESS") + .validator(is_valid_pubkey) + .takes_value(true) + .help( + "Specify the mint authority address. \ + Defaults to the client keypair address." + ), + ) + .arg( + Arg::with_name("decimals") + .long("decimals") + .validator(is_mint_decimals) + .value_name("DECIMALS") + .takes_value(true) + .default_value(default_decimals) + .help("Number of base 10 digits to the right of the decimal place"), + ) + .arg( + Arg::with_name("enable_freeze") + .long("enable-freeze") + .takes_value(false) + .help( + "Enable the mint authority to freeze token accounts for this mint" + ), + ) + .arg( + Arg::with_name("enable_close") + .long("enable-close") + .takes_value(false) + .help( + "Enable the mint authority to close this mint" + ), + ) + .arg( + Arg::with_name("interest_rate") + .long("interest-rate") + .value_name("RATE_BPS") + .takes_value(true) + .help( + "Specify the interest rate in basis points. \ + Rate authority defaults to the mint authority." + ), + ) + .arg( + Arg::with_name("metadata_address") + .long("metadata-address") + .value_name("ADDRESS") + .takes_value(true) + .conflicts_with("enable_metadata") + .help( + "Specify address that stores token metadata." + ), + ) + .arg( + Arg::with_name("enable_non_transferable") + .long("enable-non-transferable") + .alias("enable-nontransferable") + .takes_value(false) + .help( + "Permanently force tokens to be non-transferable. Thay may still be burned." + ), + ) + .arg( + Arg::with_name("default_account_state") + .long("default-account-state") + .requires("enable_freeze") + .takes_value(true) + .possible_values(&["initialized", "frozen"]) + .help("Specify that accounts have a default state. \ + Note: specifying \"initialized\" adds an extension, which gives \ + the option of specifying default frozen accounts in the future. \ + This behavior is not the same as the default, which makes it \ + impossible to specify a default account state in the future."), + ) + .arg( + Arg::with_name("transfer_fee") + .long("transfer-fee") + .value_names(&["FEE_IN_BASIS_POINTS", "MAXIMUM_FEE"]) + .takes_value(true) + .number_of_values(2) + .help( + "Add a transfer fee to the mint. \ + The mint authority can set the fee and withdraw collected fees.", + ), + ) + .arg( + Arg::with_name("enable_permanent_delegate") + .long("enable-permanent-delegate") + .takes_value(false) + .help( + "Enable the mint authority to be permanent delegate for this mint" + ), + ) + .arg( + Arg::with_name("enable_confidential_transfers") + .long("enable-confidential-transfers") + .value_names(&["APPROVE-POLICY"]) + .takes_value(true) + .possible_values(&["auto", "manual"]) + .help( + "Enable accounts to make confidential transfers. If \"auto\" \ + is selected, then accounts are automatically approved to make \ + confidential transfers. If \"manual\" is selected, then the \ + confidential transfer mint authority must approve each account \ + before it can make confidential transfers." + ) + ) + .arg( + Arg::with_name("transfer_hook") + .long("transfer-hook") + .value_name("TRANSFER_HOOK_PROGRAM_ID") + .validator(is_valid_pubkey) + .takes_value(true) + .help("Enable the mint authority to set the transfer hook program for this mint"), + ) + .arg( + Arg::with_name("enable_metadata") + .long("enable-metadata") + .conflicts_with("metadata_address") + .takes_value(false) + .help("Enables metadata in the mint. The mint authority must initialize the metadata."), + ) + .nonce_args(true) + .arg(memo_arg()) + ) + .subcommand( + SubCommand::with_name(CommandName::SetInterestRate.into()) + .about("Set the interest rate for an interest-bearing token") + .arg( + Arg::with_name("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .required(true) + .help("The interest-bearing token address"), + ) + .arg( + Arg::with_name("rate") + .value_name("RATE") + .takes_value(true) + .required(true) + .help("The new interest rate in basis points"), + ) + .arg( + Arg::with_name("rate_authority") + .long("rate-authority") + .validator(is_valid_signer) + .value_name("SIGNER") + .takes_value(true) + .help( + "Specify the rate authority keypair. \ + Defaults to the client keypair address." + ) + ) + ) + .subcommand( + SubCommand::with_name(CommandName::SetTransferHook.into()) + .about("Set the transfer hook program id for a token") + .arg( + Arg::with_name("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .required(true) + .index(1) + .help("The token address with an existing transfer hook"), + ) + .arg( + Arg::with_name("new_program_id") + .validator(is_valid_pubkey) + .value_name("NEW_PROGRAM_ID") + .takes_value(true) + .required_unless("disable") + .index(2) + .help("The new transfer hook program id to set for this mint"), + ) + .arg( + Arg::with_name("disable") + .long("disable") + .takes_value(false) + .conflicts_with("new_program_id") + .help("Disable transfer hook functionality by setting the program id to None.") + ) + .arg( + Arg::with_name("authority") + .long("authority") + .alias("owner") + .validator(is_valid_signer) + .value_name("SIGNER") + .takes_value(true) + .help("Specify the authority keypair. Defaults to the client keypair address.") + ) + ) + .subcommand( + SubCommand::with_name(CommandName::InitializeMetadata.into()) + .about("Initialize metadata extension on a token mint") + .arg( + Arg::with_name("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .required(true) + .index(1) + .help("The token address with no metadata present"), + ) + .arg( + Arg::with_name("name") + .value_name("TOKEN_NAME") + .takes_value(true) + .required(true) + .index(2) + .help("The name of the token to set in metadata"), + ) + .arg( + Arg::with_name("symbol") + .value_name("TOKEN_SYMBOL") + .takes_value(true) + .required(true) + .index(3) + .help("The symbol of the token to set in metadata"), + ) + .arg( + Arg::with_name("uri") + .value_name("TOKEN_URI") + .takes_value(true) + .required(true) + .index(4) + .help("The URI of the token to set in metadata"), + ) + .arg( + Arg::with_name("mint_authority") + .long("mint-authority") + .alias("owner") + .value_name("KEYPAIR") + .validator(is_valid_signer) + .takes_value(true) + .help( + "Specify the mint authority keypair. \ + This may be a keypair file or the ASK keyword. \ + Defaults to the client keypair." + ), + ) + .arg( + Arg::with_name("update_authority") + .long("update-authority") + .value_name("ADDRESS") + .validator(is_valid_pubkey) + .takes_value(true) + .help( + "Specify the update authority address. \ + Defaults to the client keypair address." + ), + ) + ) + .subcommand( + SubCommand::with_name(CommandName::UpdateMetadata.into()) + .about("Update metadata on a token mint that has the extension") + .arg( + Arg::with_name("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .required(true) + .index(1) + .help("The token address with no metadata present"), + ) + .arg( + Arg::with_name("field") + .value_name("FIELD_NAME") + .takes_value(true) + .required(true) + .index(2) + .help("The name of the field to update. Can be a base field (\"name\", \"symbol\", or \"uri\") or any new field to add."), + ) + .arg( + Arg::with_name("value") + .value_name("VALUE_STRING") + .takes_value(true) + .index(3) + .required_unless("remove") + .help("The value for the field"), + ) + .arg( + Arg::with_name("remove") + .long("remove") + .takes_value(false) + .conflicts_with("value") + .help("Remove the key and value for the given field. Does not work with base fields: \"name\", \"symbol\", or \"uri\".") + ) + .arg( + Arg::with_name("authority") + .long("authority") + .validator(is_valid_signer) + .value_name("SIGNER") + .takes_value(true) + .help("Specify the metadata update authority keypair. Defaults to the client keypair.") + ) + .nonce_args(true) + .arg(transfer_lamports_arg()) + .offline_args_config(&SignOnlyNeedsTransferLamports{}), + ) + .subcommand( + SubCommand::with_name(CommandName::CreateAccount.into()) + .about("Create a new token account") + .arg( + Arg::with_name("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("The token that the account will hold"), + ) + .arg( + Arg::with_name("account_keypair") + .value_name("ACCOUNT_KEYPAIR") + .validator(is_valid_signer) + .takes_value(true) + .index(2) + .help( + "Specify the account keypair. \ + This may be a keypair file or the ASK keyword. \ + [default: associated token account for --owner]" + ), + ) + .arg( + Arg::with_name("immutable") + .long("immutable") + .takes_value(false) + .help( + "Lock the owner of this token account from ever being changed" + ), + ) + .arg(owner_address_arg()) + .nonce_args(true) + ) + .subcommand( + SubCommand::with_name(CommandName::CreateMultisig.into()) + .about("Create a new account describing an M:N multisignature") + .arg( + Arg::with_name("minimum_signers") + .value_name("MINIMUM_SIGNERS") + .validator(is_multisig_minimum_signers) + .takes_value(true) + .index(1) + .required(true) + .help(minimum_signers_help), + ) + .arg( + Arg::with_name("multisig_member") + .value_name("MULTISIG_MEMBER_PUBKEY") + .validator(is_valid_pubkey) + .takes_value(true) + .index(2) + .required(true) + .min_values(MIN_SIGNERS as u64) + .max_values(MAX_SIGNERS as u64) + .help(multisig_member_help), + ) + .arg( + Arg::with_name("address_keypair") + .long("address-keypair") + .value_name("ADDRESS_KEYPAIR") + .validator(is_valid_signer) + .takes_value(true) + .help( + "Specify the address keypair. \ + This may be a keypair file or the ASK keyword. \ + [default: randomly generated keypair]" + ), + ) + .nonce_args(true) + ) + .subcommand( + SubCommand::with_name(CommandName::Authorize.into()) + .about("Authorize a new signing keypair to a token or token account") + .arg( + Arg::with_name("address") + .validator(is_valid_pubkey) + .value_name("TOKEN_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("The address of the token mint or account"), + ) + .arg( + Arg::with_name("authority_type") + .value_name("AUTHORITY_TYPE") + .takes_value(true) + .possible_values(&CliAuthorityType::iter().map(Into::into).collect::>()) + .index(2) + .required(true) + .help("The new authority type. \ + Token mints support `mint`, `freeze`, and mint extension authorities; \ + Token accounts support `owner`, `close`, and account extension \ + authorities."), + ) + .arg( + Arg::with_name("new_authority") + .validator(is_valid_pubkey) + .value_name("AUTHORITY_ADDRESS") + .takes_value(true) + .index(3) + .required_unless("disable") + .help("The address of the new authority"), + ) + .arg( + Arg::with_name("authority") + .long("authority") + .alias("owner") + .value_name("KEYPAIR") + .validator(is_valid_signer) + .takes_value(true) + .help( + "Specify the current authority keypair. \ + Defaults to the client keypair." + ), + ) + .arg( + Arg::with_name("disable") + .long("disable") + .takes_value(false) + .conflicts_with("new_authority") + .help("Disable mint, freeze, or close functionality by setting authority to None.") + ) + .arg( + Arg::with_name("force") + .long("force") + .hidden(true) + .help("Force re-authorize the wallet's associate token account. Don't use this flag"), + ) + .arg(multisig_signer_arg()) + .nonce_args(true) + .offline_args(), + ) + .subcommand( + SubCommand::with_name(CommandName::Transfer.into()) + .about("Transfer tokens between accounts") + .arg( + Arg::with_name("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("Token to transfer"), + ) + .arg( + Arg::with_name("amount") + .validator(is_amount_or_all) + .value_name("TOKEN_AMOUNT") + .takes_value(true) + .index(2) + .required(true) + .help("Amount to send, in tokens; accepts keyword ALL"), + ) + .arg( + Arg::with_name("recipient") + .validator(is_valid_pubkey) + .value_name("RECIPIENT_WALLET_ADDRESS or RECIPIENT_TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .index(3) + .required(true) + .help("If a token account address is provided, use it as the recipient. \ + Otherwise assume the recipient address is a user wallet and transfer to \ + the associated token account") + ) + .arg( + Arg::with_name("from") + .validator(is_valid_pubkey) + .value_name("SENDER_TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .long("from") + .help("Specify the sending token account \ + [default: owner's associated token account]") + ) + .arg(owner_keypair_arg_with_value_name("SENDER_TOKEN_OWNER_KEYPAIR") + .help( + "Specify the owner of the sending token account. \ + This may be a keypair file or the ASK keyword. \ + Defaults to the client keypair.", + ), + ) + .arg( + Arg::with_name("allow_unfunded_recipient") + .long("allow-unfunded-recipient") + .takes_value(false) + .help("Complete the transfer even if the recipient address is not funded") + ) + .arg( + Arg::with_name("allow_empty_recipient") + .long("allow-empty-recipient") + .takes_value(false) + .hidden(true) // Deprecated, use --allow-unfunded-recipient instead + ) + .arg( + Arg::with_name("fund_recipient") + .long("fund-recipient") + .takes_value(false) + .conflicts_with("confidential") + .help("Create the associated token account for the recipient if doesn't already exist") + ) + .arg( + Arg::with_name("no_wait") + .long("no-wait") + .takes_value(false) + .help("Return signature immediately after submitting the transaction, instead of waiting for confirmations"), + ) + .arg( + Arg::with_name("allow_non_system_account_recipient") + .long("allow-non-system-account-recipient") + .takes_value(false) + .help("Send tokens to the recipient even if the recipient is not a wallet owned by System Program."), + ) + .arg( + Arg::with_name("no_recipient_is_ata_owner") + .long("no-recipient-is-ata-owner") + .takes_value(false) + .requires("sign_only") + .help("In sign-only mode, specifies that the recipient is the owner of the associated token account rather than an actual token account"), + ) + .arg( + Arg::with_name("recipient_is_ata_owner") + .long("recipient-is-ata-owner") + .takes_value(false) + .hidden(true) + .conflicts_with("no_recipient_is_ata_owner") + .requires("sign_only") + .help("recipient-is-ata-owner is now the default behavior. The option has been deprecated and will be removed in a future release."), + ) + .arg( + Arg::with_name("expected_fee") + .long("expected-fee") + .validator(is_amount) + .value_name("TOKEN_AMOUNT") + .takes_value(true) + .help("Expected fee amount collected during the transfer"), + ) + .arg( + Arg::with_name("transfer_hook_account") + .long("transfer-hook-account") + .validator(validate_transfer_hook_account) + .value_name("PUBKEY:ROLE") + .takes_value(true) + .multiple(true) + .min_values(0u64) + .help("Additional pubkey(s) required for a transfer hook and their \ + role, in the format \":\". The role must be \ + \"readonly\", \"writable\". \"readonly-signer\", or \"writable-signer\".\ + Used for offline transaction creation and signing.") + ) + .arg( + Arg::with_name("confidential") + .long("confidential") + .takes_value(false) + .conflicts_with("fund_recipient") + .help("Send tokens confidentially. Both sender and recipient accounts must \ + be pre-configured for confidential transfers.") + ) + .arg(multisig_signer_arg()) + .arg(mint_decimals_arg()) + .nonce_args(true) + .arg(memo_arg()) + .offline_args_config(&SignOnlyNeedsMintDecimals{}), + ) + .subcommand( + SubCommand::with_name(CommandName::Burn.into()) + .about("Burn tokens from an account") + .arg( + Arg::with_name("account") + .validator(is_valid_pubkey) + .value_name("TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("The token account address to burn from"), + ) + .arg( + Arg::with_name("amount") + .validator(is_amount) + .value_name("TOKEN_AMOUNT") + .takes_value(true) + .index(2) + .required(true) + .help("Amount to burn, in tokens"), + ) + .arg(owner_keypair_arg_with_value_name("TOKEN_OWNER_KEYPAIR") + .help( + "Specify the burnt token owner account. \ + This may be a keypair file or the ASK keyword. \ + Defaults to the client keypair.", + ), + ) + .arg(multisig_signer_arg()) + .mint_args() + .nonce_args(true) + .arg(memo_arg()) + .offline_args_config(&SignOnlyNeedsFullMintSpec{}), + ) + .subcommand( + SubCommand::with_name(CommandName::Mint.into()) + .about("Mint new tokens") + .arg( + Arg::with_name("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("The token to mint"), + ) + .arg( + Arg::with_name("amount") + .validator(is_amount) + .value_name("TOKEN_AMOUNT") + .takes_value(true) + .index(2) + .required(true) + .help("Amount to mint, in tokens"), + ) + .arg( + Arg::with_name("recipient") + .validator(is_valid_pubkey) + .value_name("RECIPIENT_TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .conflicts_with("recipient_owner") + .index(3) + .help("The token account address of recipient \ + [default: associated token account for --mint-authority]"), + ) + .arg( + Arg::with_name("recipient_owner") + .long("recipient-owner") + .validator(is_valid_pubkey) + .value_name("RECIPIENT_WALLET_ADDRESS") + .takes_value(true) + .conflicts_with("recipient") + .help("The owner of the recipient associated token account"), + ) + .arg( + Arg::with_name("mint_authority") + .long("mint-authority") + .alias("owner") + .value_name("KEYPAIR") + .validator(is_valid_signer) + .takes_value(true) + .help( + "Specify the mint authority keypair. \ + This may be a keypair file or the ASK keyword. \ + Defaults to the client keypair." + ), + ) + .arg(mint_decimals_arg()) + .arg(multisig_signer_arg()) + .nonce_args(true) + .arg(memo_arg()) + .offline_args_config(&SignOnlyNeedsMintDecimals{}), + ) + .subcommand( + SubCommand::with_name(CommandName::Freeze.into()) + .about("Freeze a token account") + .arg( + Arg::with_name("account") + .validator(is_valid_pubkey) + .value_name("TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("The address of the token account to freeze"), + ) + .arg( + Arg::with_name("freeze_authority") + .long("freeze-authority") + .alias("owner") + .value_name("KEYPAIR") + .validator(is_valid_signer) + .takes_value(true) + .help( + "Specify the freeze authority keypair. \ + This may be a keypair file or the ASK keyword. \ + Defaults to the client keypair." + ), + ) + .arg(mint_address_arg()) + .arg(multisig_signer_arg()) + .nonce_args(true) + .offline_args_config(&SignOnlyNeedsMintAddress{}), + ) + .subcommand( + SubCommand::with_name(CommandName::Thaw.into()) + .about("Thaw a token account") + .arg( + Arg::with_name("account") + .validator(is_valid_pubkey) + .value_name("TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("The address of the token account to thaw"), + ) + .arg( + Arg::with_name("freeze_authority") + .long("freeze-authority") + .alias("owner") + .value_name("KEYPAIR") + .validator(is_valid_signer) + .takes_value(true) + .help( + "Specify the freeze authority keypair. \ + This may be a keypair file or the ASK keyword. \ + Defaults to the client keypair." + ), + ) + .arg(mint_address_arg()) + .arg(multisig_signer_arg()) + .nonce_args(true) + .offline_args_config(&SignOnlyNeedsMintAddress{}), + ) + .subcommand( + SubCommand::with_name(CommandName::Wrap.into()) + .about("Wrap native SOL in a SOL token account") + .arg( + Arg::with_name("amount") + .validator(is_amount) + .value_name("AMOUNT") + .takes_value(true) + .index(1) + .required(true) + .help("Amount of SOL to wrap"), + ) + .arg( + Arg::with_name("wallet_keypair") + .alias("owner") + .value_name("KEYPAIR") + .validator(is_valid_signer) + .takes_value(true) + .help( + "Specify the keypair for the wallet which will have its native SOL wrapped. \ + This wallet will be assigned as the owner of the wrapped SOL token account. \ + This may be a keypair file or the ASK keyword. \ + Defaults to the client keypair." + ), + ) + .arg( + Arg::with_name("create_aux_account") + .takes_value(false) + .long("create-aux-account") + .help("Wrap SOL in an auxiliary account instead of associated token account"), + ) + .arg( + Arg::with_name("immutable") + .long("immutable") + .takes_value(false) + .help( + "Lock the owner of this token account from ever being changed" + ), + ) + .nonce_args(true) + .offline_args(), + ) + .subcommand( + SubCommand::with_name(CommandName::Unwrap.into()) + .about("Unwrap a SOL token account") + .arg( + Arg::with_name("account") + .validator(is_valid_pubkey) + .value_name("TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .index(1) + .help("The address of the auxiliary token account to unwrap \ + [default: associated token account for --owner]"), + ) + .arg( + Arg::with_name("wallet_keypair") + .alias("owner") + .value_name("KEYPAIR") + .validator(is_valid_signer) + .takes_value(true) + .help( + "Specify the keypair for the wallet which owns the wrapped SOL. \ + This wallet will receive the unwrapped SOL. \ + This may be a keypair file or the ASK keyword. \ + Defaults to the client keypair." + ), + ) + .arg(owner_address_arg()) + .arg(multisig_signer_arg()) + .nonce_args(true) + .offline_args(), + ) + .subcommand( + SubCommand::with_name(CommandName::Approve.into()) + .about("Approve a delegate for a token account") + .arg( + Arg::with_name("account") + .validator(is_valid_pubkey) + .value_name("TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("The address of the token account to delegate"), + ) + .arg( + Arg::with_name("amount") + .validator(is_amount) + .value_name("TOKEN_AMOUNT") + .takes_value(true) + .index(2) + .required(true) + .help("Amount to approve, in tokens"), + ) + .arg( + Arg::with_name("delegate") + .validator(is_valid_pubkey) + .value_name("DELEGATE_TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .index(3) + .required(true) + .help("The token account address of delegate"), + ) + .arg( + owner_keypair_arg() + ) + .arg(multisig_signer_arg()) + .mint_args() + .nonce_args(true) + .offline_args_config(&SignOnlyNeedsFullMintSpec{}), + ) + .subcommand( + SubCommand::with_name(CommandName::Revoke.into()) + .about("Revoke a delegate's authority") + .arg( + Arg::with_name("account") + .validator(is_valid_pubkey) + .value_name("TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("The address of the token account"), + ) + .arg(owner_keypair_arg() + ) + .arg(delegate_address_arg()) + .arg(multisig_signer_arg()) + .nonce_args(true) + .offline_args_config(&SignOnlyNeedsDelegateAddress{}), + ) + .subcommand( + SubCommand::with_name(CommandName::Close.into()) + .about("Close a token account") + .arg( + Arg::with_name("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .index(1) + .required_unless("address") + .help("Token of the associated account to close. \ + To close a specific account, use the `--address` parameter instead"), + ) + .arg( + Arg::with_name("recipient") + .long("recipient") + .validator(is_valid_pubkey) + .value_name("REFUND_ACCOUNT_ADDRESS") + .takes_value(true) + .help("The address of the account to receive remaining SOL [default: --owner]"), + ) + .arg( + Arg::with_name("close_authority") + .long("close-authority") + .alias("owner") + .value_name("KEYPAIR") + .validator(is_valid_signer) + .takes_value(true) + .help( + "Specify the token's close authority if it has one, \ + otherwise specify the token's owner keypair. \ + This may be a keypair file or the ASK keyword. \ + Defaults to the client keypair.", + ), + ) + .arg( + Arg::with_name("address") + .long("address") + .validator(is_valid_pubkey) + .value_name("TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .conflicts_with("token") + .help("Specify the token account to close \ + [default: owner's associated token account]"), + ) + .arg(owner_address_arg()) + .arg(multisig_signer_arg()) + .nonce_args(true) + ) + .subcommand( + SubCommand::with_name(CommandName::CloseMint.into()) + .about("Close a token mint") + .arg( + Arg::with_name("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("Token to close"), + ) + .arg( + Arg::with_name("recipient") + .long("recipient") + .validator(is_valid_pubkey) + .value_name("REFUND_ACCOUNT_ADDRESS") + .takes_value(true) + .help("The address of the account to receive remaining SOL [default: --owner]"), + ) + .arg( + Arg::with_name("close_authority") + .long("close-authority") + .value_name("KEYPAIR") + .validator(is_valid_signer) + .takes_value(true) + .help( + "Specify the token's close authority. \ + This may be a keypair file or the ASK keyword. \ + Defaults to the client keypair.", + ), + ) + .arg(owner_address_arg()) + .arg(multisig_signer_arg()) + .nonce_args(true) + .offline_args(), + ) + .subcommand( + SubCommand::with_name(CommandName::Balance.into()) + .about("Get token account balance") + .arg( + Arg::with_name("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .index(1) + .required_unless("address") + .help("Token of associated account. To query a specific account, use the `--address` parameter instead"), + ) + .arg(owner_address_arg().conflicts_with("address")) + .arg( + Arg::with_name("address") + .validator(is_valid_pubkey) + .value_name("TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .long("address") + .conflicts_with("token") + .help("Specify the token account to query \ + [default: owner's associated token account]"), + ), + ) + .subcommand( + SubCommand::with_name(CommandName::Supply.into()) + .about("Get token supply") + .arg( + Arg::with_name("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("The token address"), + ), + ) + .subcommand( + SubCommand::with_name(CommandName::Accounts.into()) + .about("List all token accounts by owner") + .arg( + Arg::with_name("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .index(1) + .help("Limit results to the given token. [Default: list accounts for all tokens]"), + ) + .arg( + Arg::with_name("delegated") + .long("delegated") + .takes_value(false) + .conflicts_with("externally_closeable") + .help( + "Limit results to accounts with transfer delegations" + ), + ) + .arg( + Arg::with_name("externally_closeable") + .long("externally-closeable") + .takes_value(false) + .conflicts_with("delegated") + .help( + "Limit results to accounts with external close authorities" + ), + ) + .arg( + Arg::with_name("addresses_only") + .long("addresses-only") + .takes_value(false) + .conflicts_with("verbose") + .conflicts_with("output_format") + .help( + "Print token account addresses only" + ), + ) + .arg(owner_address_arg()) + ) + .subcommand( + SubCommand::with_name(CommandName::Address.into()) + .about("Get wallet address") + .arg( + Arg::with_name("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .long("token") + .requires("verbose") + .help("Return the associated token address for the given token. \ + [Default: return the client keypair address]") + ) + .arg( + owner_address_arg() + .requires("token") + .help("Return the associated token address for the given owner. \ + [Default: return the associated token address for the client keypair]"), + ), + ) + .subcommand( + SubCommand::with_name(CommandName::AccountInfo.into()) + .about("Query details of an SPL Token account by address (DEPRECATED: use `spl-token display`)") + .setting(AppSettings::Hidden) + .arg( + Arg::with_name("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .index(1) + .conflicts_with("address") + .required_unless("address") + .help("Token of associated account. \ + To query a specific account, use the `--address` parameter instead"), + ) + .arg( + Arg::with_name(OWNER_ADDRESS_ARG.name) + .takes_value(true) + .value_name("OWNER_ADDRESS") + .validator(is_valid_signer) + .help(OWNER_ADDRESS_ARG.help) + .index(2) + .conflicts_with("address") + .help("Owner of the associated account for the specified token. \ + To query a specific account, use the `--address` parameter instead. \ + Defaults to the client keypair."), + ) + .arg( + Arg::with_name("address") + .validator(is_valid_pubkey) + .value_name("TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .long("address") + .conflicts_with("token") + .help("Specify the token account to query"), + ), + ) + .subcommand( + SubCommand::with_name(CommandName::MultisigInfo.into()) + .about("Query details of an SPL Token multisig account by address (DEPRECATED: use `spl-token display`)") + .setting(AppSettings::Hidden) + .arg( + Arg::with_name("address") + .validator(is_valid_pubkey) + .value_name("MULTISIG_ACCOUNT_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("The address of the SPL Token multisig account to query"), + ), + ) + .subcommand( + SubCommand::with_name(CommandName::Display.into()) + .about("Query details of an SPL Token mint, account, or multisig by address") + .arg( + Arg::with_name("address") + .validator(is_valid_pubkey) + .value_name("TOKEN_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("The address of the SPL Token mint, account, or multisig to query"), + ), + ) + .subcommand( + SubCommand::with_name(CommandName::Gc.into()) + .about("Cleanup unnecessary token accounts") + .arg(owner_keypair_arg()) + .arg( + Arg::with_name("close_empty_associated_accounts") + .long("close-empty-associated-accounts") + .takes_value(false) + .help("close all empty associated token accounts (to get SOL back)") + ) + ) + .subcommand( + SubCommand::with_name(CommandName::SyncNative.into()) + .about("Sync a native SOL token account to its underlying lamports") + .arg( + owner_address_arg() + .index(1) + .conflicts_with("address") + .help("Owner of the associated account for the native token. \ + To query a specific account, use the `--address` parameter instead. \ + Defaults to the client keypair."), + ) + .arg( + Arg::with_name("address") + .validator(is_valid_pubkey) + .value_name("TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .long("address") + .conflicts_with("owner") + .help("Specify the specific token account address to sync"), + ), + ) + .subcommand( + SubCommand::with_name(CommandName::EnableRequiredTransferMemos.into()) + .about("Enable required transfer memos for token account") + .arg( + Arg::with_name("account") + .validator(is_valid_pubkey) + .value_name("TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("The address of the token account to require transfer memos for") + ) + .arg( + owner_address_arg() + ) + .arg(multisig_signer_arg()) + .nonce_args(true) + ) + .subcommand( + SubCommand::with_name(CommandName::DisableRequiredTransferMemos.into()) + .about("Disable required transfer memos for token account") + .arg( + Arg::with_name("account") + .validator(is_valid_pubkey) + .value_name("TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("The address of the token account to stop requiring transfer memos for"), + ) + .arg( + owner_address_arg() + ) + .arg(multisig_signer_arg()) + .nonce_args(true) + ) + .subcommand( + SubCommand::with_name(CommandName::EnableCpiGuard.into()) + .about("Enable CPI Guard for token account") + .arg( + Arg::with_name("account") + .validator(is_valid_pubkey) + .value_name("TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("The address of the token account to enable CPI Guard for") + ) + .arg( + owner_address_arg() + ) + .arg(multisig_signer_arg()) + .nonce_args(true) + ) + .subcommand( + SubCommand::with_name(CommandName::DisableCpiGuard.into()) + .about("Disable CPI Guard for token account") + .arg( + Arg::with_name("account") + .validator(is_valid_pubkey) + .value_name("TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("The address of the token account to disable CPI Guard for"), + ) + .arg( + owner_address_arg() + ) + .arg(multisig_signer_arg()) + .nonce_args(true) + ) + .subcommand( + SubCommand::with_name(CommandName::UpdateDefaultAccountState.into()) + .about("Updates default account state for the mint. Requires the default account state extension.") + .arg( + Arg::with_name("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("The address of the token mint to update default account state"), + ) + .arg( + Arg::with_name("state") + .value_name("STATE") + .takes_value(true) + .possible_values(&["initialized", "frozen"]) + .index(2) + .required(true) + .help("The new default account state."), + ) + .arg( + Arg::with_name("freeze_authority") + .long("freeze-authority") + .value_name("KEYPAIR") + .validator(is_valid_signer) + .takes_value(true) + .help( + "Specify the token's freeze authority. \ + This may be a keypair file or the ASK keyword. \ + Defaults to the client keypair.", + ), + ) + .arg(owner_address_arg()) + .arg(multisig_signer_arg()) + .nonce_args(true) + .offline_args(), + ) + .subcommand( + SubCommand::with_name(CommandName::UpdateMetadataAddress.into()) + .about("Updates metadata pointer address for the mint. Requires the metadata pointer extension.") + .arg( + Arg::with_name("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("The address of the token mint to update the metadata pointer address"), + ) + .arg( + Arg::with_name("metadata_address") + .index(2) + .validator(is_valid_pubkey) + .value_name("METADATA_ADDRESS") + .takes_value(true) + .required_unless("disable") + .help("Specify address that stores token's metadata-pointer"), + ) + .arg( + Arg::with_name("disable") + .long("disable") + .takes_value(false) + .conflicts_with("metadata_address") + .help("Unset metadata pointer address.") + ) + .arg( + Arg::with_name("authority") + .long("authority") + .value_name("KEYPAIR") + .validator(is_valid_signer) + .takes_value(true) + .help( + "Specify the token's metadata-pointer authority. \ + This may be a keypair file or the ASK keyword. \ + Defaults to the client keypair.", + ), + ) + .arg(multisig_signer_arg()) + .nonce_args(true) + ) + .subcommand( + SubCommand::with_name(CommandName::WithdrawWithheldTokens.into()) + .about("Withdraw withheld transfer fee tokens from mint and / or account(s)") + .arg( + Arg::with_name("account") + .validator(is_valid_pubkey) + .value_name("TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("The address of the token account to receive withdrawn tokens"), + ) + .arg( + Arg::with_name("source") + .validator(is_valid_pubkey) + .value_name("ACCOUNT_ADDRESS") + .takes_value(true) + .multiple(true) + .min_values(0u64) + .help("The token accounts to withdraw from") + ) + .arg( + Arg::with_name("include_mint") + .long("include-mint") + .takes_value(false) + .help("Also withdraw withheld tokens from the mint"), + ) + .arg( + Arg::with_name("withdraw_withheld_authority") + .long("withdraw-withheld-authority") + .alias("owner") + .value_name("KEYPAIR") + .validator(is_valid_signer) + .takes_value(true) + .help( + "Specify the withdraw withheld authority keypair. \ + This may be a keypair file or the ASK keyword. \ + Defaults to the client keypair." + ), + ) + .arg(owner_address_arg()) + .arg(multisig_signer_arg()) + ) + .subcommand( + SubCommand::with_name(CommandName::SetTransferFee.into()) + .about("Set the transfer fee for a token with a configured transfer fee") + .arg( + Arg::with_name("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .required(true) + .help("The interest-bearing token address"), + ) + .arg( + Arg::with_name("transfer_fee_basis_points") + .value_name("FEE_IN_BASIS_POINTS") + .takes_value(true) + .required(true) + .help("The new transfer fee in basis points"), + ) + .arg( + Arg::with_name("maximum_fee") + .value_name("TOKEN_AMOUNT") + .validator(is_amount) + .takes_value(true) + .required(true) + .help("The new maximum transfer fee in UI amount"), + ) + .arg( + Arg::with_name("transfer_fee_authority") + .long("transfer-fee-authority") + .validator(is_valid_signer) + .value_name("SIGNER") + .takes_value(true) + .help( + "Specify the rate authority keypair. \ + Defaults to the client keypair address." + ) + ) + .arg(mint_decimals_arg()) + .offline_args_config(&SignOnlyNeedsMintDecimals{}) + ) + .subcommand( + SubCommand::with_name(CommandName::WithdrawExcessLamports.into()) + .about("Withdraw lamports from a Token Program owned account") + .arg( + Arg::with_name("from") + .validator(is_valid_pubkey) + .value_name("SOURCE_ACCOUNT_ADDRESS") + .takes_value(true) + .required(true) + .help("Specify the address of the account to recover lamports from"), + ) + .arg( + Arg::with_name("recipient") + .validator(is_valid_pubkey) + .value_name("REFUND_ACCOUNT_ADDRESS") + .takes_value(true) + .required(true) + .help("Specify the address of the account to send lamports to"), + ) + .arg(owner_address_arg()) + .arg(multisig_signer_arg()) + ) + .subcommand( + SubCommand::with_name(CommandName::UpdateConfidentialTransferSettings.into()) + .about("Update confidential transfer configuation for a token") + .arg( + Arg::with_name("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("The address of the token mint to update confidential transfer configuration for") + ) + .arg( + Arg::with_name("approve_policy") + .long("approve-policy") + .value_name("APPROVE_POLICY") + .takes_value(true) + .possible_values(&["auto", "manual"]) + .help( + "Policy for enabling accounts to make confidential transfers. If \"auto\" \ + is selected, then accounts are automatically approved to make \ + confidential transfers. If \"manual\" is selected, then the \ + confidential transfer mint authority must approve each account \ + before it can make confidential transfers." + ) + ) + .arg( + Arg::with_name("auditor_pubkey") + .long("auditor-pubkey") + .value_name("AUDITOR_PUBKEY") + .takes_value(true) + .help( + "The auditor encryption public key. The corresponding private key for \ + this auditor public key can be used to decrypt all confidential \ + transfers involving tokens from this mint. Currently, the auditor \ + public key can only be specified as a direct *base64* encoding of \ + an ElGamal public key. More methods of specifying the auditor public \ + key will be supported in a future version. To disable auditability \ + feature for the token, use \"none\"." + ) + ) + .group( + ArgGroup::with_name("update_fields").args(&["approve_policy", "auditor_pubkey"]) + .required(true) + .multiple(true) + ) + .arg( + Arg::with_name("confidential_transfer_authority") + .long("confidential-transfer-authority") + .validator(is_valid_signer) + .value_name("SIGNER") + .takes_value(true) + .help( + "Specify the confidential transfer authority keypair. \ + Defaults to the client keypair address." + ) + ) + .nonce_args(true) + .offline_args(), + ) + .subcommand( + SubCommand::with_name(CommandName::ConfigureConfidentialTransferAccount.into()) + .about("Configure confidential transfers for token account") + .arg( + Arg::with_name("token") + .long("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .index(1) + .required_unless("address") + .help("The token address with confidential transfers enabled"), + ) + .arg( + Arg::with_name("address") + .long("address") + .validator(is_valid_pubkey) + .value_name("TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .conflicts_with("token") + .help("The address of the token account to configure confidential transfers for \ + [default: owner's associated token account]") + ) + .arg( + owner_address_arg() + ) + .arg( + Arg::with_name("maximum_pending_balance_credit_counter") + .long("maximum-pending-balance-credit-counter") + .value_name("MAXIMUM-CREDIT-COUNTER") + .takes_value(true) + .help( + "The maximum pending balance credit counter. \ + This parameter limits the number of confidential transfers that a token account \ + can receive to facilitate decryption of the encrypted balance. \ + Defaults to 65536 (2^16)" + ) + ) + .arg(multisig_signer_arg()) + .nonce_args(true) + ) + .subcommand( + SubCommand::with_name(CommandName::EnableConfidentialCredits.into()) + .about("Enable confidential transfers for token account. To enable confidential transfers \ + for the first time, use `configure-confidential-transfer-account` instead.") + .arg( + Arg::with_name("token") + .long("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .index(1) + .required_unless("address") + .help("The token address with confidential transfers enabled"), + ) + .arg( + Arg::with_name("address") + .long("address") + .validator(is_valid_pubkey) + .value_name("TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .conflicts_with("token") + .help("The address of the token account to enable confidential transfers for \ + [default: owner's associated token account]") + ) + .arg( + owner_address_arg() + ) + .arg(multisig_signer_arg()) + .nonce_args(true) + ) + .subcommand( + SubCommand::with_name(CommandName::DisableConfidentialCredits.into()) + .about("Disable confidential transfers for token account") + .arg( + Arg::with_name("token") + .long("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .index(1) + .required_unless("address") + .help("The token address with confidential transfers enabled"), + ) + .arg( + Arg::with_name("address") + .long("address") + .validator(is_valid_pubkey) + .value_name("TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .conflicts_with("token") + .help("The address of the token account to disable confidential transfers for \ + [default: owner's associated token account]") + ) + .arg( + owner_address_arg() + ) + .arg(multisig_signer_arg()) + .nonce_args(true) + ) + .subcommand( + SubCommand::with_name(CommandName::EnableNonConfidentialCredits.into()) + .about("Enable non-confidential transfers for token account.") + .arg( + Arg::with_name("token") + .long("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .index(1) + .required_unless("address") + .help("The token address with confidential transfers enabled"), + ) + .arg( + Arg::with_name("address") + .long("address") + .validator(is_valid_pubkey) + .value_name("TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .conflicts_with("token") + .help("The address of the token account to enable non-confidential transfers for \ + [default: owner's associated token account]") + ) + .arg( + owner_address_arg() + ) + .arg(multisig_signer_arg()) + .nonce_args(true) + ) + .subcommand( + SubCommand::with_name(CommandName::DisableNonConfidentialCredits.into()) + .about("Disable non-confidential transfers for token account") + .arg( + Arg::with_name("token") + .long("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .index(1) + .required_unless("address") + .help("The token address with confidential transfers enabled"), + ) + .arg( + Arg::with_name("address") + .long("address") + .validator(is_valid_pubkey) + .value_name("TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .conflicts_with("token") + .help("The address of the token account to disable non-confidential transfers for \ + [default: owner's associated token account]") + ) + .arg( + owner_address_arg() + ) + .arg(multisig_signer_arg()) + .nonce_args(true) + ) + .subcommand( + SubCommand::with_name(CommandName::DepositConfidentialTokens.into()) + .about("Deposit amounts for confidential transfers") + .arg( + Arg::with_name("token") + .long("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("The token address with confidential transfers enabled"), + ) + .arg( + Arg::with_name("amount") + .validator(is_amount_or_all) + .value_name("TOKEN_AMOUNT") + .takes_value(true) + .index(2) + .required(true) + .help("Amount to deposit; accepts keyword ALL"), + ) + .arg( + Arg::with_name("address") + .long("address") + .validator(is_valid_pubkey) + .value_name("TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .help("The address of the token account to configure confidential transfers for \ + [default: owner's associated token account]") + ) + .arg( + owner_address_arg() + ) + .arg(multisig_signer_arg()) + .arg(mint_decimals_arg()) + .nonce_args(true) + ) + .subcommand( + SubCommand::with_name(CommandName::WithdrawConfidentialTokens.into()) + .about("Withdraw amounts for confidential transfers") + .arg( + Arg::with_name("token") + .long("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .index(1) + .required(true) + .help("The token address with confidential transfers enabled"), + ) + .arg( + Arg::with_name("amount") + .validator(is_amount_or_all) + .value_name("TOKEN_AMOUNT") + .takes_value(true) + .index(2) + .required(true) + .help("Amount to deposit; accepts keyword ALL"), + ) + .arg( + Arg::with_name("address") + .long("address") + .validator(is_valid_pubkey) + .value_name("TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .help("The address of the token account to configure confidential transfers for \ + [default: owner's associated token account]") + ) + .arg( + owner_address_arg() + ) + .arg(multisig_signer_arg()) + .arg(mint_decimals_arg()) + .nonce_args(true) + ) + .subcommand( + SubCommand::with_name(CommandName::ApplyPendingBalance.into()) + .about("Collect confidential tokens from pending to available balance") + .arg( + Arg::with_name("token") + .long("token") + .validator(is_valid_pubkey) + .value_name("TOKEN_MINT_ADDRESS") + .takes_value(true) + .index(1) + .required_unless("address") + .help("The token address with confidential transfers enabled"), + ) + .arg( + Arg::with_name("address") + .long("address") + .validator(is_valid_pubkey) + .value_name("TOKEN_ACCOUNT_ADDRESS") + .takes_value(true) + .help("The address of the token account to configure confidential transfers for \ + [default: owner's associated token account]") + ) + .arg( + owner_address_arg() + ) + .arg(multisig_signer_arg()) + .nonce_args(true) + ) +} diff --git a/token/cli/src/command.rs b/token/cli/src/command.rs new file mode 100644 index 00000000000..7cb728c1fd6 --- /dev/null +++ b/token/cli/src/command.rs @@ -0,0 +1,4226 @@ +#![allow(clippy::arithmetic_side_effects)] +use { + crate::{ + bench::*, + clap_app::*, + config::{Config, MintInfo}, + encryption_keypair::*, + output::*, + sort::{sort_and_parse_token_accounts, AccountFilter}, + }, + clap::{value_t, value_t_or_exit, ArgMatches}, + futures::try_join, + serde::Serialize, + solana_account_decoder::{ + parse_token::{get_token_account_mint, parse_token, TokenAccountType, UiAccountState}, + UiAccountData, + }, + solana_clap_utils::{ + input_parsers::{pubkey_of_signer, pubkeys_of_multiple_signers, value_of}, + keypair::signer_from_path, + }, + solana_cli_output::{ + return_signers_data, CliSignOnlyData, CliSignature, OutputFormat, QuietDisplay, + ReturnSignersConfig, VerboseDisplay, + }, + solana_client::rpc_request::TokenAccountsFilter, + solana_remote_wallet::remote_wallet::RemoteWalletManager, + solana_sdk::{ + instruction::AccountMeta, + native_token::*, + program_option::COption, + pubkey::Pubkey, + signature::{Keypair, Signer}, + system_program, + }, + spl_associated_token_account::get_associated_token_address_with_program_id, + spl_token_2022::{ + extension::{ + confidential_transfer::{ + account_info::{ + ApplyPendingBalanceAccountInfo, TransferAccountInfo, WithdrawAccountInfo, + }, + instruction::TransferSplitContextStateAccounts, + ConfidentialTransferAccount, ConfidentialTransferMint, + }, + confidential_transfer_fee::ConfidentialTransferFeeConfig, + cpi_guard::CpiGuard, + default_account_state::DefaultAccountState, + interest_bearing_mint::InterestBearingConfig, + memo_transfer::MemoTransfer, + metadata_pointer::MetadataPointer, + mint_close_authority::MintCloseAuthority, + permanent_delegate::PermanentDelegate, + transfer_fee::{TransferFeeAmount, TransferFeeConfig}, + transfer_hook::TransferHook, + BaseStateWithExtensions, ExtensionType, StateWithExtensionsOwned, + }, + solana_zk_token_sdk::{ + encryption::{ + auth_encryption::AeKey, + elgamal::{self, ElGamalKeypair}, + }, + zk_token_elgamal::pod::ElGamalPubkey, + }, + state::{Account, AccountState, Mint}, + }, + spl_token_client::{ + client::{ProgramRpcClientSendTransaction, RpcClientResponse}, + token::{ExtensionInitializationParams, Token}, + }, + spl_token_metadata_interface::state::{Field, TokenMetadata}, + std::{collections::HashMap, fmt::Display, process::exit, rc::Rc, str::FromStr, sync::Arc}, +}; + +fn print_error_and_exit(e: E) -> T { + eprintln!("error: {}", e); + exit(1) +} + +type BulkSigners = Vec>; +pub type CommandResult = Result; + +fn push_signer_with_dedup(signer: Arc, bulk_signers: &mut BulkSigners) { + if !bulk_signers.contains(&signer) { + bulk_signers.push(signer); + } +} + +fn new_throwaway_signer() -> (Arc, Pubkey) { + let keypair = Keypair::new(); + let pubkey = keypair.pubkey(); + (Arc::new(keypair) as Arc, pubkey) +} + +fn get_signer( + matches: &ArgMatches<'_>, + keypair_name: &str, + wallet_manager: &mut Option>, +) -> Option<(Arc, Pubkey)> { + matches.value_of(keypair_name).map(|path| { + let signer = signer_from_path(matches, path, keypair_name, wallet_manager) + .unwrap_or_else(print_error_and_exit); + let signer_pubkey = signer.pubkey(); + (Arc::from(signer), signer_pubkey) + }) +} +async fn check_wallet_balance( + config: &Config<'_>, + wallet: &Pubkey, + required_balance: u64, +) -> Result<(), Error> { + let balance = config.rpc_client.get_balance(wallet).await?; + if balance < required_balance { + Err(format!( + "Wallet {}, has insufficient balance: {} required, {} available", + wallet, + lamports_to_sol(required_balance), + lamports_to_sol(balance) + ) + .into()) + } else { + Ok(()) + } +} + +fn token_client_from_config( + config: &Config<'_>, + token_pubkey: &Pubkey, + decimals: Option, +) -> Result, Error> { + let token = Token::new( + config.program_client.clone(), + &config.program_id, + token_pubkey, + decimals, + config.fee_payer()?.clone(), + ); + + if let (Some(nonce_account), Some(nonce_authority), Some(nonce_blockhash)) = ( + config.nonce_account, + &config.nonce_authority, + config.nonce_blockhash, + ) { + Ok(token.with_nonce( + &nonce_account, + Arc::clone(nonce_authority), + &nonce_blockhash, + )) + } else { + Ok(token) + } +} + +fn native_token_client_from_config( + config: &Config<'_>, +) -> Result, Error> { + let token = Token::new_native( + config.program_client.clone(), + &config.program_id, + config.fee_payer()?.clone(), + ); + + if let (Some(nonce_account), Some(nonce_authority), Some(nonce_blockhash)) = ( + config.nonce_account, + &config.nonce_authority, + config.nonce_blockhash, + ) { + Ok(token.with_nonce( + &nonce_account, + Arc::clone(nonce_authority), + &nonce_blockhash, + )) + } else { + Ok(token) + } +} + +#[allow(clippy::too_many_arguments)] +async fn command_create_token( + config: &Config<'_>, + decimals: u8, + token_pubkey: Pubkey, + authority: Pubkey, + enable_freeze: bool, + enable_close: bool, + enable_non_transferable: bool, + enable_permanent_delegate: bool, + memo: Option, + metadata_address: Option, + rate_bps: Option, + default_account_state: Option, + transfer_fee: Option<(u16, u64)>, + confidential_transfer_auto_approve: Option, + transfer_hook_program_id: Option, + enable_metadata: bool, + bulk_signers: Vec>, +) -> CommandResult { + println_display( + config, + format!( + "Creating token {} under program {}", + token_pubkey, config.program_id + ), + ); + + let token = token_client_from_config(config, &token_pubkey, Some(decimals))?; + + let freeze_authority = if enable_freeze { Some(authority) } else { None }; + + let mut extensions = vec![]; + + if enable_close { + extensions.push(ExtensionInitializationParams::MintCloseAuthority { + close_authority: Some(authority), + }); + } + + if enable_permanent_delegate { + extensions.push(ExtensionInitializationParams::PermanentDelegate { + delegate: authority, + }); + } + + if let Some(rate_bps) = rate_bps { + extensions.push(ExtensionInitializationParams::InterestBearingConfig { + rate_authority: Some(authority), + rate: rate_bps, + }) + } + + if enable_non_transferable { + extensions.push(ExtensionInitializationParams::NonTransferable); + } + + if let Some(state) = default_account_state { + assert!( + enable_freeze, + "Token requires a freeze authority to default to frozen accounts" + ); + extensions.push(ExtensionInitializationParams::DefaultAccountState { state }) + } + + if let Some((transfer_fee_basis_points, maximum_fee)) = transfer_fee { + extensions.push(ExtensionInitializationParams::TransferFeeConfig { + transfer_fee_config_authority: Some(authority), + withdraw_withheld_authority: Some(authority), + transfer_fee_basis_points, + maximum_fee, + }); + } + + if let Some(auto_approve) = confidential_transfer_auto_approve { + extensions.push(ExtensionInitializationParams::ConfidentialTransferMint { + authority: Some(authority), + auto_approve_new_accounts: auto_approve, + auditor_elgamal_pubkey: None, + }); + } + + if let Some(program_id) = transfer_hook_program_id { + extensions.push(ExtensionInitializationParams::TransferHook { + authority: Some(authority), + program_id: Some(program_id), + }); + } + + if let Some(text) = memo { + token.with_memo(text, vec![config.default_signer()?.pubkey()]); + } + + // CLI checks that only one is set + if metadata_address.is_some() || enable_metadata { + let metadata_address = if enable_metadata { + Some(token_pubkey) + } else { + metadata_address + }; + extensions.push(ExtensionInitializationParams::MetadataPointer { + authority: Some(authority), + metadata_address, + }); + } + + let res = token + .create_mint( + &authority, + freeze_authority.as_ref(), + extensions, + &bulk_signers, + ) + .await?; + + let tx_return = finish_tx(config, &res, false).await?; + + if enable_metadata { + println_display( + config, + format!( + "To initialize metadata inside the mint, please run \ + `spl-token initialize-metadata {token_pubkey} `, \ + and sign with the mint authority.", + ), + ); + } + + Ok(match tx_return { + TransactionReturnData::CliSignature(cli_signature) => format_output( + CliCreateToken { + address: token_pubkey.to_string(), + decimals, + transaction_data: cli_signature, + }, + &CommandName::CreateToken, + config, + ), + TransactionReturnData::CliSignOnlyData(cli_sign_only_data) => { + format_output(cli_sign_only_data, &CommandName::CreateToken, config) + } + }) +} + +async fn command_set_interest_rate( + config: &Config<'_>, + token_pubkey: Pubkey, + rate_authority: Pubkey, + rate_bps: i16, + bulk_signers: Vec>, +) -> CommandResult { + let token = token_client_from_config(config, &token_pubkey, None)?; + + if !config.sign_only { + let mint_account = config.get_account_checked(&token_pubkey).await?; + + let mint_state = StateWithExtensionsOwned::::unpack(mint_account.data) + .map_err(|_| format!("Could not deserialize token mint {}", token_pubkey))?; + + if let Ok(interest_rate_config) = mint_state.get_extension::() { + let mint_rate_authority_pubkey = + Option::::from(interest_rate_config.rate_authority); + + if mint_rate_authority_pubkey != Some(rate_authority) { + return Err(format!( + "Mint {} has interest rate authority {}, but {} was provided", + token_pubkey, + mint_rate_authority_pubkey + .map(|pubkey| pubkey.to_string()) + .unwrap_or_else(|| "disabled".to_string()), + rate_authority + ) + .into()); + } + } else { + return Err(format!("Mint {} is not interest-bearing", token_pubkey).into()); + } + } + + println_display( + config, + format!( + "Setting Interest Rate for {} to {} bps", + token_pubkey, rate_bps + ), + ); + + let res = token + .update_interest_rate(&rate_authority, rate_bps, &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_set_transfer_hook_program( + config: &Config<'_>, + token_pubkey: Pubkey, + authority: Pubkey, + new_program_id: Option, + bulk_signers: Vec>, +) -> CommandResult { + let token = token_client_from_config(config, &token_pubkey, None)?; + + if !config.sign_only { + let mint_account = config.get_account_checked(&token_pubkey).await?; + + let mint_state = StateWithExtensionsOwned::::unpack(mint_account.data) + .map_err(|_| format!("Could not deserialize token mint {}", token_pubkey))?; + + if let Ok(extension) = mint_state.get_extension::() { + let authority_pubkey = Option::::from(extension.authority); + + if authority_pubkey != Some(authority) { + return Err(format!( + "Mint {} has transfer hook authority {}, but {} was provided", + token_pubkey, + authority_pubkey + .map(|pubkey| pubkey.to_string()) + .unwrap_or_else(|| "disabled".to_string()), + authority + ) + .into()); + } + } else { + return Err( + format!("Mint {} does not have permissioned-transfers", token_pubkey).into(), + ); + } + } + + println_display( + config, + format!( + "Setting Transfer Hook Program id for {} to {}", + token_pubkey, + new_program_id + .map(|pubkey| pubkey.to_string()) + .unwrap_or_else(|| "disabled".to_string()) + ), + ); + + let res = token + .update_transfer_hook_program_id(&authority, new_program_id, &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) + } + }) +} + +#[allow(clippy::too_many_arguments)] +async fn command_initialize_metadata( + config: &Config<'_>, + token_pubkey: Pubkey, + update_authority: Pubkey, + mint_authority: Pubkey, + name: String, + symbol: String, + uri: String, + bulk_signers: Vec>, +) -> CommandResult { + let token = token_client_from_config(config, &token_pubkey, None)?; + + let res = token + .token_metadata_initialize_with_rent_transfer( + &config.fee_payer()?.pubkey(), + &update_authority, + &mint_authority, + name, + symbol, + uri, + &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_update_metadata( + config: &Config<'_>, + token_pubkey: Pubkey, + authority: Pubkey, + field: Field, + value: Option, + transfer_lamports: Option, + bulk_signers: Vec>, +) -> CommandResult { + let token = token_client_from_config(config, &token_pubkey, None)?; + + let res = if let Some(value) = value { + token + .token_metadata_update_field_with_rent_transfer( + &config.fee_payer()?.pubkey(), + &authority, + field, + value, + transfer_lamports, + &bulk_signers, + ) + .await? + } else if let Field::Key(key) = field { + token + .token_metadata_remove_key( + &authority, + key, + true, // idempotent + &bulk_signers, + ) + .await? + } else { + return Err(format!( + "Attempting to remove field {field:?}, which cannot be removed. \ + Please re-run the command with a value of \"\" rather than the `--remove` flag." + ) + .into()); + }; + + 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_set_transfer_fee( + config: &Config<'_>, + token_pubkey: Pubkey, + transfer_fee_authority: Pubkey, + transfer_fee_basis_points: u16, + maximum_fee: f64, + mint_decimals: Option, + bulk_signers: Vec>, +) -> CommandResult { + let decimals = if !config.sign_only { + let mint_account = config.get_account_checked(&token_pubkey).await?; + + let mint_state = StateWithExtensionsOwned::::unpack(mint_account.data) + .map_err(|_| format!("Could not deserialize token mint {}", token_pubkey))?; + + if mint_decimals.is_some() && mint_decimals != Some(mint_state.base.decimals) { + return Err(format!( + "Decimals {} was provided, but actual value is {}", + mint_decimals.unwrap(), + mint_state.base.decimals + ) + .into()); + } + + if let Ok(transfer_fee_config) = mint_state.get_extension::() { + let mint_fee_authority_pubkey = + Option::::from(transfer_fee_config.transfer_fee_config_authority); + + if mint_fee_authority_pubkey != Some(transfer_fee_authority) { + return Err(format!( + "Mint {} has transfer fee authority {}, but {} was provided", + token_pubkey, + mint_fee_authority_pubkey + .map(|pubkey| pubkey.to_string()) + .unwrap_or_else(|| "disabled".to_string()), + transfer_fee_authority + ) + .into()); + } + } else { + return Err(format!("Mint {} does not have a transfer fee", token_pubkey).into()); + } + mint_state.base.decimals + } else { + mint_decimals.unwrap() + }; + + println_display( + config, + format!( + "Setting transfer fee for {} to {} bps, {} maximum", + token_pubkey, transfer_fee_basis_points, maximum_fee + ), + ); + + let token = token_client_from_config(config, &token_pubkey, Some(decimals))?; + let maximum_fee = spl_token::ui_amount_to_amount(maximum_fee, decimals); + let res = token + .set_transfer_fee( + &transfer_fee_authority, + transfer_fee_basis_points, + maximum_fee, + &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_create_account( + config: &Config<'_>, + token_pubkey: Pubkey, + owner: Pubkey, + maybe_account: Option, + immutable_owner: bool, + bulk_signers: Vec>, +) -> CommandResult { + let token = token_client_from_config(config, &token_pubkey, None)?; + let mut extensions = vec![]; + + let (account, is_associated) = if let Some(account) = maybe_account { + ( + account, + token.get_associated_token_address(&owner) == account, + ) + } else { + (token.get_associated_token_address(&owner), true) + }; + + println_display(config, format!("Creating account {}", account)); + + if !config.sign_only { + if let Some(account_data) = config.program_client.get_account(account).await? { + if account_data.owner != system_program::id() || !is_associated { + return Err(format!("Error: Account already exists: {}", account).into()); + } + } + } + + if immutable_owner { + if config.program_id == spl_token::id() { + return Err(format!( + "Specified --immutable, but token program {} does not support the extension", + config.program_id + ) + .into()); + } else if is_associated { + println_display( + config, + "Note: --immutable specified, but Token-2022 ATAs are always immutable, ignoring" + .to_string(), + ); + } else { + extensions.push(ExtensionType::ImmutableOwner); + } + } + + let res = if is_associated { + token.create_associated_token_account(&owner).await + } else { + let signer = bulk_signers + .iter() + .find(|signer| signer.pubkey() == account) + .unwrap_or_else(|| panic!("No signer provided for account {}", account)); + + token + .create_auxiliary_token_account_with_extension_space(&**signer, &owner, extensions) + .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_create_multisig( + config: &Config<'_>, + multisig: Arc, + minimum_signers: u8, + multisig_members: Vec, +) -> CommandResult { + println_display( + config, + format!( + "Creating {}/{} multisig {} under program {}", + minimum_signers, + multisig_members.len(), + multisig.pubkey(), + config.program_id, + ), + ); + + // default is safe here because create_multisig doesnt use it + let token = token_client_from_config(config, &Pubkey::default(), None)?; + + let res = token + .create_multisig( + &*multisig, + &multisig_members.iter().collect::>(), + minimum_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) + } + }) +} + +#[allow(clippy::too_many_arguments)] +async fn command_authorize( + config: &Config<'_>, + account: Pubkey, + authority_type: CliAuthorityType, + authority: Pubkey, + new_authority: Option, + force_authorize: bool, + bulk_signers: BulkSigners, +) -> CommandResult { + let auth_str: &'static str = (&authority_type).into(); + + let (mint_pubkey, previous_authority) = if !config.sign_only { + let target_account = config.get_account_checked(&account).await?; + + let (mint_pubkey, previous_authority) = if let Ok(mint) = + StateWithExtensionsOwned::::unpack(target_account.data.clone()) + { + let previous_authority = match authority_type { + CliAuthorityType::Owner | CliAuthorityType::Close => Err(format!( + "Authority type `{}` not supported for SPL Token mints", + auth_str + )), + CliAuthorityType::Mint => Ok(Option::::from(mint.base.mint_authority)), + CliAuthorityType::Freeze => Ok(Option::::from(mint.base.freeze_authority)), + CliAuthorityType::CloseMint => { + if let Ok(mint_close_authority) = mint.get_extension::() { + Ok(Option::::from(mint_close_authority.close_authority)) + } else { + Err(format!( + "Mint `{}` does not support close authority", + account + )) + } + } + CliAuthorityType::TransferFeeConfig => { + if let Ok(transfer_fee_config) = mint.get_extension::() { + Ok(Option::::from( + transfer_fee_config.transfer_fee_config_authority, + )) + } else { + Err(format!("Mint `{}` does not support transfer fees", account)) + } + } + CliAuthorityType::WithheldWithdraw => { + if let Ok(transfer_fee_config) = mint.get_extension::() { + Ok(Option::::from( + transfer_fee_config.withdraw_withheld_authority, + )) + } else { + Err(format!("Mint `{}` does not support transfer fees", account)) + } + } + CliAuthorityType::InterestRate => { + if let Ok(interest_rate_config) = mint.get_extension::() + { + Ok(Option::::from(interest_rate_config.rate_authority)) + } else { + Err(format!("Mint `{}` is not interest-bearing", account)) + } + } + CliAuthorityType::PermanentDelegate => { + if let Ok(permanent_delegate) = mint.get_extension::() { + Ok(Option::::from(permanent_delegate.delegate)) + } else { + Err(format!( + "Mint `{}` does not support permanent delegate", + account + )) + } + } + CliAuthorityType::ConfidentialTransferMint => { + if let Ok(confidential_transfer_mint) = + mint.get_extension::() + { + Ok(Option::::from(confidential_transfer_mint.authority)) + } else { + Err(format!( + "Mint `{}` does not support confidential transfers", + account + )) + } + } + CliAuthorityType::TransferHookProgramId => { + if let Ok(extension) = mint.get_extension::() { + Ok(Option::::from(extension.authority)) + } else { + Err(format!( + "Mint `{}` does not support a transfer hook program", + account + )) + } + } + CliAuthorityType::ConfidentialTransferFee => { + if let Ok(confidential_transfer_fee_config) = + mint.get_extension::() + { + Ok(Option::::from( + confidential_transfer_fee_config.authority, + )) + } else { + Err(format!( + "Mint `{}` does not support confidential transfer fees", + account + )) + } + } + CliAuthorityType::MetadataPointer => { + if let Ok(extension) = mint.get_extension::() { + Ok(Option::::from(extension.authority)) + } else { + Err(format!( + "Mint `{}` does not support a metadata pointer", + account + )) + } + } + CliAuthorityType::Metadata => { + if let Ok(extension) = mint.get_variable_len_extension::() { + Ok(Option::::from(extension.update_authority)) + } else { + Err(format!("Mint `{account}` does not support metadata")) + } + } + }?; + + Ok((account, previous_authority)) + } else if let Ok(token_account) = + StateWithExtensionsOwned::::unpack(target_account.data) + { + let check_associated_token_account = || -> Result<(), Error> { + let maybe_associated_token_account = get_associated_token_address_with_program_id( + &token_account.base.owner, + &token_account.base.mint, + &config.program_id, + ); + if account == maybe_associated_token_account + && !force_authorize + && Some(authority) != new_authority + { + Err(format!( + "Error: attempting to change the `{}` of an associated token account", + auth_str + ) + .into()) + } else { + Ok(()) + } + }; + + let previous_authority = match authority_type { + CliAuthorityType::Mint + | CliAuthorityType::Freeze + | CliAuthorityType::CloseMint + | CliAuthorityType::TransferFeeConfig + | CliAuthorityType::WithheldWithdraw + | CliAuthorityType::InterestRate + | CliAuthorityType::PermanentDelegate + | CliAuthorityType::ConfidentialTransferMint + | CliAuthorityType::TransferHookProgramId + | CliAuthorityType::ConfidentialTransferFee + | CliAuthorityType::MetadataPointer + | CliAuthorityType::Metadata => Err(format!( + "Authority type `{auth_str}` not supported for SPL Token accounts", + )), + CliAuthorityType::Owner => { + check_associated_token_account()?; + Ok(Some(token_account.base.owner)) + } + CliAuthorityType::Close => { + check_associated_token_account()?; + Ok(Some( + token_account + .base + .close_authority + .unwrap_or(token_account.base.owner), + )) + } + }?; + + Ok((token_account.base.mint, previous_authority)) + } else { + Err("Unsupported account data format".to_string()) + }?; + + (mint_pubkey, previous_authority) + } else { + // default is safe here because authorize doesnt use it + (Pubkey::default(), None) + }; + + let token = token_client_from_config(config, &mint_pubkey, None)?; + + println_display( + config, + format!( + "Updating {}\n Current {}: {}\n New {}: {}", + account, + auth_str, + previous_authority + .map(|pubkey| pubkey.to_string()) + .unwrap_or_else(|| if config.sign_only { + "unknown".to_string() + } else { + "disabled".to_string() + }), + auth_str, + new_authority + .map(|pubkey| pubkey.to_string()) + .unwrap_or_else(|| "disabled".to_string()) + ), + ); + + let res = if let CliAuthorityType::Metadata = authority_type { + token + .token_metadata_update_authority(&authority, new_authority, &bulk_signers) + .await? + } else { + token + .set_authority( + &account, + &authority, + new_authority.as_ref(), + authority_type.try_into()?, + &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) + } + }) +} + +#[allow(clippy::too_many_arguments)] +async fn command_transfer( + config: &Config<'_>, + token_pubkey: Pubkey, + ui_amount: Option, + recipient: Pubkey, + sender: Option, + sender_owner: Pubkey, + allow_unfunded_recipient: bool, + fund_recipient: bool, + mint_decimals: Option, + no_recipient_is_ata_owner: bool, + use_unchecked_instruction: bool, + ui_fee: Option, + memo: Option, + bulk_signers: BulkSigners, + no_wait: bool, + allow_non_system_account_recipient: bool, + transfer_hook_accounts: Option>, + confidential_transfer_args: Option<&ConfidentialTransferArgs>, +) -> CommandResult { + let mint_info = config.get_mint_info(&token_pubkey, mint_decimals).await?; + + // if the user got the decimals wrong, they may well have calculated the + // transfer amount wrong we only check in online mode, because in offline, + // mint_info.decimals is always 9 + if !config.sign_only && mint_decimals.is_some() && mint_decimals != Some(mint_info.decimals) { + return Err(format!( + "Decimals {} was provided, but actual value is {}", + mint_decimals.unwrap(), + mint_info.decimals + ) + .into()); + } + + // decimals determines whether transfer_checked is used or not + // in online mode, mint_decimals may be None but mint_info.decimals is always + // correct in offline mode, mint_info.decimals may be wrong, but + // mint_decimals is always provided and in online mode, when mint_decimals + // is provided, it is verified correct hence the fallthrough logic here + let decimals = if use_unchecked_instruction { + None + } else if mint_decimals.is_some() { + mint_decimals + } else { + Some(mint_info.decimals) + }; + + let token = if let Some(transfer_hook_accounts) = transfer_hook_accounts { + token_client_from_config(config, &token_pubkey, decimals)? + .with_transfer_hook_accounts(transfer_hook_accounts) + } else { + token_client_from_config(config, &token_pubkey, decimals)? + }; + + // pubkey of the actual account we are sending from + let sender = if let Some(sender) = sender { + sender + } else { + token.get_associated_token_address(&sender_owner) + }; + + // the amount the user wants to tranfer, as a f64 + let maybe_transfer_balance = + ui_amount.map(|ui_amount| spl_token::ui_amount_to_amount(ui_amount, mint_info.decimals)); + + // the amount we will transfer, as a u64 + let transfer_balance = if !config.sign_only { + let sender_balance = token.get_account_info(&sender).await?.base.amount; + let transfer_balance = maybe_transfer_balance.unwrap_or(sender_balance); + + println_display( + config, + format!( + "{}Transfer {} tokens\n Sender: {}\n Recipient: {}", + if confidential_transfer_args.is_some() { + "Confidential " + } else { + "" + }, + spl_token::amount_to_ui_amount(transfer_balance, mint_info.decimals), + sender, + recipient + ), + ); + + if transfer_balance > sender_balance && confidential_transfer_args.is_none() { + return Err(format!( + "Error: Sender has insufficient funds, current balance is {}", + spl_token_2022::amount_to_ui_amount_string_trimmed( + sender_balance, + mint_info.decimals + ) + ) + .into()); + } + + transfer_balance + } else { + maybe_transfer_balance.unwrap() + }; + + let maybe_fee = + ui_fee.map(|ui_amount| spl_token::ui_amount_to_amount(ui_amount, mint_info.decimals)); + + // determine whether recipient is a token account or an expected owner of one + let recipient_is_token_account = if !config.sign_only { + // in online mode we can fetch it and see + let maybe_recipient_account_data = config.program_client.get_account(recipient).await?; + + // if the account exists, and: + // * its a token for this program, we are happy + // * its a system account, we are happy + // * its a non-account for this program, we error helpfully + // * its a token account for a different program, we error helpfully + // * otherwise its probabaly a program account owner of an ata, in which case we + // gate transfer with a flag + if let Some(recipient_account_data) = maybe_recipient_account_data { + let recipient_account_owner = recipient_account_data.owner; + let maybe_account_state = + StateWithExtensionsOwned::::unpack(recipient_account_data.data); + + if recipient_account_owner == config.program_id && maybe_account_state.is_ok() { + if let Ok(memo_transfer) = maybe_account_state?.get_extension::() { + if memo_transfer.require_incoming_transfer_memos.into() && memo.is_none() { + return Err( + "Error: Recipient expects a transfer memo, but none was provided. \ + Provide a memo using `--with-memo`." + .into(), + ); + } + } + + true + } else if recipient_account_owner == system_program::id() { + false + } else if recipient_account_owner == config.program_id { + return Err( + "Error: Recipient is owned by this token program, but is not a token account." + .into(), + ); + } else if VALID_TOKEN_PROGRAM_IDS.contains(&recipient_account_owner) { + return Err(format!( + "Error: Recipient is owned by {}, but the token mint is owned by {}.", + recipient_account_owner, config.program_id + ) + .into()); + } else if allow_non_system_account_recipient { + false + } else { + return Err("Error: The recipient address is not owned by the System Program. \ + Add `--allow-non-system-account-recipient` to complete the transfer.".into()); + } + } + // if it doesnt exist, it definitely isnt a token account! + // we gate transfer with a different flag + else if maybe_recipient_account_data.is_none() && allow_unfunded_recipient { + false + } else { + return Err("Error: The recipient address is not funded. \ + Add `--allow-unfunded-recipient` to complete the transfer." + .into()); + } + } else { + // in offline mode we gotta trust them + no_recipient_is_ata_owner + }; + + // now if its a token account, life is ez + let (recipient_token_account, fundable_owner) = if recipient_is_token_account { + (recipient, None) + } + // but if not, we need to determine if we can or should create an ata for recipient + else { + // first, get the ata address + let recipient_token_account = token.get_associated_token_address(&recipient); + + println_display( + config, + format!( + " Recipient associated token account: {}", + recipient_token_account + ), + ); + + // if we can fetch it to determine if it exists, do so + let needs_funding = if !config.sign_only { + if let Some(recipient_token_account_data) = config + .program_client + .get_account(recipient_token_account) + .await? + { + let recipient_token_account_owner = recipient_token_account_data.owner; + + if let Ok(account_state) = + StateWithExtensionsOwned::::unpack(recipient_token_account_data.data) + { + if let Ok(memo_transfer) = account_state.get_extension::() { + if memo_transfer.require_incoming_transfer_memos.into() && memo.is_none() { + return Err( + "Error: Recipient expects a transfer memo, but none was provided. \ + Provide a memo using `--with-memo`." + .into(), + ); + } + } + } + + if recipient_token_account_owner == system_program::id() { + true + } else if recipient_token_account_owner == config.program_id { + false + } else { + return Err( + format!("Error: Unsupported recipient address: {}", recipient).into(), + ); + } + } else { + true + } + } + // otherwise trust the cli flag + else { + fund_recipient + }; + + // and now we determine if we will actually fund it, based on its need and our + // willingness + let fundable_owner = if needs_funding { + if confidential_transfer_args.is_some() { + return Err( + "Error: Recipient's associated token account does not exist. \ + Accounts cannot be funded for confidential transfers." + .into(), + ); + } else if fund_recipient { + println_display( + config, + format!(" Funding recipient: {}", recipient_token_account,), + ); + + Some(recipient) + } else { + return Err( + "Error: Recipient's associated token account does not exist. \ + Add `--fund-recipient` to fund their account" + .into(), + ); + } + } else { + None + }; + + (recipient_token_account, fundable_owner) + }; + + // set up memo if provided... + if let Some(text) = memo { + token.with_memo(text, vec![config.default_signer()?.pubkey()]); + } + + // fetch confidential transfer info for recipient and auditor + let (recipient_elgamal_pubkey, auditor_elgamal_pubkey) = if let Some(args) = + confidential_transfer_args + { + if !config.sign_only { + // we can use the mint data from the start of the function, but will require + // non-trivial amount of refactoring the code due to ownership; for now, we + // fetch the mint a second time. This can potentially be optimized + // in the future. + let confidential_transfer_mint = config.get_account_checked(&token_pubkey).await?; + let mint_state = + StateWithExtensionsOwned::::unpack(confidential_transfer_mint.data) + .map_err(|_| format!("Could not deserialize token mint {}", token_pubkey))?; + + let auditor_elgamal_pubkey = if let Ok(confidential_transfer_mint) = + mint_state.get_extension::() + { + let expected_auditor_elgamal_pubkey = Option::::from( + confidential_transfer_mint.auditor_elgamal_pubkey, + ); + + // if auditor ElGamal pubkey is provided, check consistency with the one in the + // mint if auditor ElGamal pubkey is not provided, then use the + // expected one from the mint, which could also be `None` if + // auditing is disabled + if args.auditor_elgamal_pubkey.is_some() + && expected_auditor_elgamal_pubkey != args.auditor_elgamal_pubkey + { + return Err(format!( + "Mint {} has confidential transfer auditor {}, but {} was provided", + token_pubkey, + expected_auditor_elgamal_pubkey + .map(|pubkey| pubkey.to_string()) + .unwrap_or_else(|| "disabled".to_string()), + args.auditor_elgamal_pubkey.unwrap(), + ) + .into()); + } + + expected_auditor_elgamal_pubkey + } else { + return Err(format!( + "Mint {} does not support confidential transfers", + token_pubkey + ) + .into()); + }; + + let recipient_account = config.get_account_checked(&recipient_token_account).await?; + let recipient_elgamal_pubkey = + StateWithExtensionsOwned::::unpack(recipient_account.data)? + .get_extension::()? + .elgamal_pubkey; + + (Some(recipient_elgamal_pubkey), auditor_elgamal_pubkey) + } else { + let recipient_elgamal_pubkey = args + .recipient_elgamal_pubkey + .expect("Recipient ElGamal pubkey must be provided"); + let auditor_elgamal_pubkey = args + .auditor_elgamal_pubkey + .expect("Auditor ElGamal pubkey must be provided"); + + (Some(recipient_elgamal_pubkey), Some(auditor_elgamal_pubkey)) + } + } else { + (None, None) + }; + + // ...and, finally, the transfer + let res = match (fundable_owner, maybe_fee, confidential_transfer_args) { + (Some(recipient_owner), None, None) => { + token + .create_recipient_associated_account_and_transfer( + &sender, + &recipient_token_account, + &recipient_owner, + &sender_owner, + transfer_balance, + maybe_fee, + &bulk_signers, + ) + .await? + } + (Some(_), _, _) => { + panic!("Recipient account cannot be created for transfer with fees or confidential transfers"); + } + (None, Some(fee), None) => { + token + .transfer_with_fee( + &sender, + &recipient_token_account, + &sender_owner, + transfer_balance, + fee, + &bulk_signers, + ) + .await? + } + (None, None, Some(args)) => { + // deserialize `pod` ElGamal pubkeys + let recipient_elgamal_pubkey: elgamal::ElGamalPubkey = recipient_elgamal_pubkey + .unwrap() + .try_into() + .expect("Invalid recipient ElGamal pubkey"); + let auditor_elgamal_pubkey = auditor_elgamal_pubkey.map(|pubkey| { + let auditor_elgamal_pubkey: elgamal::ElGamalPubkey = + pubkey.try_into().expect("Invalid auditor ElGamal pubkey"); + auditor_elgamal_pubkey + }); + + let context_state_authority = config.fee_payer()?; + let equality_proof_context_state_account = Keypair::new(); + let equality_proof_pubkey = equality_proof_context_state_account.pubkey(); + let ciphertext_validity_proof_context_state_account = Keypair::new(); + let ciphertext_validity_proof_pubkey = + ciphertext_validity_proof_context_state_account.pubkey(); + let range_proof_context_state_account = Keypair::new(); + let range_proof_pubkey = range_proof_context_state_account.pubkey(); + + let transfer_context_state_accounts = TransferSplitContextStateAccounts { + equality_proof: &equality_proof_pubkey, + ciphertext_validity_proof: &ciphertext_validity_proof_pubkey, + range_proof: &range_proof_pubkey, + authority: &context_state_authority.pubkey(), + no_op_on_uninitialized_split_context_state: false, + close_split_context_state_accounts: None, + }; + + let state = token.get_account_info(&sender).await.unwrap(); + let extension = state + .get_extension::() + .unwrap(); + let transfer_account_info = TransferAccountInfo::new(extension); + + let ( + equality_proof_data, + ciphertext_validity_proof_data, + range_proof_data, + source_decrypt_handles, + ) = transfer_account_info + .generate_split_transfer_proof_data( + transfer_balance, + &args.sender_elgamal_keypair, + &args.sender_aes_key, + &recipient_elgamal_pubkey, + auditor_elgamal_pubkey.as_ref(), + ) + .unwrap(); + + // setup proofs + let _ = try_join!( + token.create_range_proof_context_state_for_transfer( + transfer_context_state_accounts, + &range_proof_data, + &range_proof_context_state_account, + ), + token.create_equality_proof_context_state_for_transfer( + transfer_context_state_accounts, + &equality_proof_data, + &equality_proof_context_state_account, + ), + token.create_ciphertext_validity_proof_context_state_for_transfer( + transfer_context_state_accounts, + &ciphertext_validity_proof_data, + &ciphertext_validity_proof_context_state_account, + ) + )?; + + // do the transfer + let transfer_result = token + .confidential_transfer_transfer_with_split_proofs( + &sender, + &recipient_token_account, + &sender_owner, + transfer_context_state_accounts, + transfer_balance, + Some(transfer_account_info), + &args.sender_aes_key, + &source_decrypt_handles, + &bulk_signers, + ) + .await?; + + // close context state accounts + let context_state_authority_pubkey = context_state_authority.pubkey(); + let close_context_state_signers = &[context_state_authority]; + let _ = try_join!( + token.confidential_transfer_close_context_state( + &equality_proof_pubkey, + &sender, + &context_state_authority_pubkey, + close_context_state_signers, + ), + token.confidential_transfer_close_context_state( + &ciphertext_validity_proof_pubkey, + &sender, + &context_state_authority_pubkey, + close_context_state_signers, + ), + token.confidential_transfer_close_context_state( + &range_proof_pubkey, + &sender, + &context_state_authority_pubkey, + close_context_state_signers, + ), + )?; + + transfer_result + } + (None, Some(_), Some(_)) => { + panic!("Confidential transfer with fee is not yet supported."); + } + (None, None, None) => { + token + .transfer( + &sender, + &recipient_token_account, + &sender_owner, + transfer_balance, + &bulk_signers, + ) + .await? + } + }; + + let tx_return = finish_tx(config, &res, no_wait).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) + } + }) +} + +#[allow(clippy::too_many_arguments)] +async fn command_burn( + config: &Config<'_>, + account: Pubkey, + owner: Pubkey, + ui_amount: f64, + mint_address: Option, + mint_decimals: Option, + use_unchecked_instruction: bool, + memo: Option, + bulk_signers: BulkSigners, +) -> CommandResult { + println_display( + config, + format!("Burn {} tokens\n Source: {}", ui_amount, account), + ); + + let mint_address = config.check_account(&account, mint_address).await?; + let mint_info = config.get_mint_info(&mint_address, mint_decimals).await?; + let amount = spl_token::ui_amount_to_amount(ui_amount, mint_info.decimals); + let decimals = if use_unchecked_instruction { + None + } else { + Some(mint_info.decimals) + }; + + let token = token_client_from_config(config, &mint_info.address, decimals)?; + if let Some(text) = memo { + token.with_memo(text, vec![config.default_signer()?.pubkey()]); + } + + let res = token.burn(&account, &owner, amount, &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) + } + }) +} + +#[allow(clippy::too_many_arguments)] +async fn command_mint( + config: &Config<'_>, + token: Pubkey, + ui_amount: f64, + recipient: Pubkey, + mint_info: MintInfo, + mint_authority: Pubkey, + use_unchecked_instruction: bool, + memo: Option, + bulk_signers: BulkSigners, +) -> CommandResult { + println_display( + config, + format!( + "Minting {} tokens\n Token: {}\n Recipient: {}", + ui_amount, token, recipient + ), + ); + + let amount = spl_token::ui_amount_to_amount(ui_amount, mint_info.decimals); + let decimals = if use_unchecked_instruction { + None + } else { + Some(mint_info.decimals) + }; + + let token = token_client_from_config(config, &mint_info.address, decimals)?; + if let Some(text) = memo { + token.with_memo(text, vec![config.default_signer()?.pubkey()]); + } + + let res = token + .mint_to(&recipient, &mint_authority, amount, &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_freeze( + config: &Config<'_>, + account: Pubkey, + mint_address: Option, + freeze_authority: Pubkey, + bulk_signers: BulkSigners, +) -> CommandResult { + let mint_address = config.check_account(&account, mint_address).await?; + let mint_info = config.get_mint_info(&mint_address, None).await?; + + println_display( + config, + format!( + "Freezing account: {}\n Token: {}", + account, mint_info.address + ), + ); + + // we dont use the decimals from mint_info because its not need and in sign-only + // its wrong + let token = token_client_from_config(config, &mint_info.address, None)?; + let res = token + .freeze(&account, &freeze_authority, &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_thaw( + config: &Config<'_>, + account: Pubkey, + mint_address: Option, + freeze_authority: Pubkey, + bulk_signers: BulkSigners, +) -> CommandResult { + let mint_address = config.check_account(&account, mint_address).await?; + let mint_info = config.get_mint_info(&mint_address, None).await?; + + println_display( + config, + format!( + "Thawing account: {}\n Token: {}", + account, mint_info.address + ), + ); + + // we dont use the decimals from mint_info because its not need and in sign-only + // its wrong + let token = token_client_from_config(config, &mint_info.address, None)?; + let res = token + .thaw(&account, &freeze_authority, &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_wrap( + config: &Config<'_>, + sol: f64, + wallet_address: Pubkey, + wrapped_sol_account: Option, + immutable_owner: bool, + bulk_signers: BulkSigners, +) -> CommandResult { + let lamports = sol_to_lamports(sol); + let token = native_token_client_from_config(config)?; + + let account = + wrapped_sol_account.unwrap_or_else(|| token.get_associated_token_address(&wallet_address)); + + println_display(config, format!("Wrapping {} SOL into {}", sol, account)); + + if !config.sign_only { + if let Some(account_data) = config.program_client.get_account(account).await? { + if account_data.owner != system_program::id() { + return Err(format!("Error: Account already exists: {}", account).into()); + } + } + + check_wallet_balance(config, &wallet_address, lamports).await?; + } + + let res = if immutable_owner { + if config.program_id == spl_token::id() { + return Err(format!( + "Specified --immutable, but token program {} does not support the extension", + config.program_id + ) + .into()); + } + + token + .wrap(&account, &wallet_address, lamports, &bulk_signers) + .await? + } else { + // this case is hit for a token22 ata, which is always immutable. but it does + // the right thing anyway + token + .wrap_with_mutable_ownership(&account, &wallet_address, lamports, &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_unwrap( + config: &Config<'_>, + wallet_address: Pubkey, + maybe_account: Option, + bulk_signers: BulkSigners, +) -> CommandResult { + let use_associated_account = maybe_account.is_none(); + let token = native_token_client_from_config(config)?; + + let account = + maybe_account.unwrap_or_else(|| token.get_associated_token_address(&wallet_address)); + + println_display(config, format!("Unwrapping {}", account)); + + if !config.sign_only { + let account_data = config.get_account_checked(&account).await?; + + if !use_associated_account { + let account_state = StateWithExtensionsOwned::::unpack(account_data.data)?; + + if account_state.base.mint != *token.get_address() { + return Err(format!("{} is not a native token account", account).into()); + } + } + + if account_data.lamports == 0 { + if use_associated_account { + return Err("No wrapped SOL in associated account; did you mean to specify an auxiliary address?".to_string().into()); + } else { + return Err(format!("No wrapped SOL in {}", account).into()); + } + } + + println_display( + config, + format!(" Amount: {} SOL", lamports_to_sol(account_data.lamports)), + ); + } + + println_display(config, format!(" Recipient: {}", &wallet_address)); + + let res = token + .close_account(&account, &wallet_address, &wallet_address, &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) + } + }) +} + +#[allow(clippy::too_many_arguments)] +async fn command_approve( + config: &Config<'_>, + account: Pubkey, + owner: Pubkey, + ui_amount: f64, + delegate: Pubkey, + mint_address: Option, + mint_decimals: Option, + use_unchecked_instruction: bool, + bulk_signers: BulkSigners, +) -> CommandResult { + println_display( + config, + format!( + "Approve {} tokens\n Account: {}\n Delegate: {}", + ui_amount, account, delegate + ), + ); + + let mint_address = config.check_account(&account, mint_address).await?; + let mint_info = config.get_mint_info(&mint_address, mint_decimals).await?; + let amount = spl_token::ui_amount_to_amount(ui_amount, mint_info.decimals); + let decimals = if use_unchecked_instruction { + None + } else { + Some(mint_info.decimals) + }; + + let token = token_client_from_config(config, &mint_info.address, decimals)?; + let res = token + .approve(&account, &delegate, &owner, amount, &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_revoke( + config: &Config<'_>, + account: Pubkey, + owner: Pubkey, + delegate: Option, + bulk_signers: BulkSigners, +) -> CommandResult { + let (mint_pubkey, delegate) = if !config.sign_only { + let source_account = config.get_account_checked(&account).await?; + let source_state = StateWithExtensionsOwned::::unpack(source_account.data) + .map_err(|_| format!("Could not deserialize token account {}", account))?; + + let delegate = if let COption::Some(delegate) = source_state.base.delegate { + Some(delegate) + } else { + None + }; + + (source_state.base.mint, delegate) + } else { + // default is safe here because revoke doesnt use it + (Pubkey::default(), delegate) + }; + + if let Some(delegate) = delegate { + println_display( + config, + format!( + "Revoking approval\n Account: {}\n Delegate: {}", + account, delegate + ), + ); + } else { + return Err(format!("No delegate on account {}", account).into()); + } + + let token = token_client_from_config(config, &mint_pubkey, None)?; + let res = token.revoke(&account, &owner, &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_close( + config: &Config<'_>, + account: Pubkey, + close_authority: Pubkey, + recipient: Pubkey, + bulk_signers: BulkSigners, +) -> CommandResult { + let mut results = vec![]; + let token = if !config.sign_only { + let source_account = config.get_account_checked(&account).await?; + + let source_state = StateWithExtensionsOwned::::unpack(source_account.data) + .map_err(|_| format!("Could not deserialize token account {}", account))?; + let source_amount = source_state.base.amount; + + if !source_state.base.is_native() && source_amount > 0 { + return Err(format!( + "Account {} still has {} tokens; empty the account in order to close it.", + account, source_amount, + ) + .into()); + } + + let token = token_client_from_config(config, &source_state.base.mint, None)?; + if let Ok(extension) = source_state.get_extension::() { + if u64::from(extension.withheld_amount) != 0 { + let res = token.harvest_withheld_tokens_to_mint(&[&account]).await?; + let tx_return = finish_tx(config, &res, false).await?; + results.push(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) + } + }); + } + } + + token + } else { + // default is safe here because close doesnt use it + token_client_from_config(config, &Pubkey::default(), None)? + }; + + let res = token + .close_account(&account, &recipient, &close_authority, &bulk_signers) + .await?; + + let tx_return = finish_tx(config, &res, false).await?; + results.push(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) + } + }); + Ok(results.join("")) +} + +async fn command_close_mint( + config: &Config<'_>, + token_pubkey: Pubkey, + close_authority: Pubkey, + recipient: Pubkey, + bulk_signers: BulkSigners, +) -> CommandResult { + if !config.sign_only { + let mint_account = config.get_account_checked(&token_pubkey).await?; + + let mint_state = StateWithExtensionsOwned::::unpack(mint_account.data) + .map_err(|_| format!("Could not deserialize token mint {}", token_pubkey))?; + let mint_supply = mint_state.base.supply; + + if mint_supply > 0 { + return Err(format!( + "Mint {} still has {} outstanding tokens; these must be burned before closing the mint.", + token_pubkey, mint_supply, + ) + .into()); + } + + if let Ok(mint_close_authority) = mint_state.get_extension::() { + let mint_close_authority_pubkey = + Option::::from(mint_close_authority.close_authority); + + if mint_close_authority_pubkey != Some(close_authority) { + return Err(format!( + "Mint {} has close authority {}, but {} was provided", + token_pubkey, + mint_close_authority_pubkey + .map(|pubkey| pubkey.to_string()) + .unwrap_or_else(|| "disabled".to_string()), + close_authority + ) + .into()); + } + } else { + return Err(format!("Mint {} does not support close authority", token_pubkey).into()); + } + } + + let token = token_client_from_config(config, &token_pubkey, None)?; + let res = token + .close_account(&token_pubkey, &recipient, &close_authority, &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_balance(config: &Config<'_>, address: Pubkey) -> CommandResult { + let balance = config + .rpc_client + .get_token_account_balance(&address) + .await + .map_err(|_| format!("Could not find token account {}", address))?; + let cli_token_amount = CliTokenAmount { amount: balance }; + Ok(config.output_format.formatted_string(&cli_token_amount)) +} + +async fn command_supply(config: &Config<'_>, token: Pubkey) -> CommandResult { + let supply = config.rpc_client.get_token_supply(&token).await?; + let cli_token_amount = CliTokenAmount { amount: supply }; + Ok(config.output_format.formatted_string(&cli_token_amount)) +} + +async fn command_accounts( + config: &Config<'_>, + maybe_token: Option, + owner: Pubkey, + account_filter: AccountFilter, + print_addresses_only: bool, +) -> CommandResult { + let filters = if let Some(token_pubkey) = maybe_token { + let _ = config.get_mint_info(&token_pubkey, None).await?; + vec![TokenAccountsFilter::Mint(token_pubkey)] + } else if config.restrict_to_program_id { + vec![TokenAccountsFilter::ProgramId(config.program_id)] + } else { + vec![ + TokenAccountsFilter::ProgramId(spl_token::id()), + TokenAccountsFilter::ProgramId(spl_token_2022::id()), + ] + }; + + let mut accounts = vec![]; + for filter in filters { + accounts.push( + config + .rpc_client + .get_token_accounts_by_owner(&owner, filter) + .await?, + ); + } + let accounts = accounts.into_iter().flatten().collect(); + + let cli_token_accounts = + sort_and_parse_token_accounts(&owner, accounts, maybe_token.is_some(), account_filter)?; + + if print_addresses_only { + Ok(cli_token_accounts + .accounts + .into_iter() + .flatten() + .map(|a| a.address) + .collect::>() + .join("\n")) + } else { + Ok(config.output_format.formatted_string(&cli_token_accounts)) + } +} + +async fn command_address( + config: &Config<'_>, + token: Option, + owner: Pubkey, +) -> CommandResult { + let mut cli_address = CliWalletAddress { + wallet_address: owner.to_string(), + ..CliWalletAddress::default() + }; + if let Some(token) = token { + config.get_mint_info(&token, None).await?; + let associated_token_address = + get_associated_token_address_with_program_id(&owner, &token, &config.program_id); + cli_address.associated_token_address = Some(associated_token_address.to_string()); + } + Ok(config.output_format.formatted_string(&cli_address)) +} + +async fn command_display(config: &Config<'_>, address: Pubkey) -> CommandResult { + let account_data = config.get_account_checked(&address).await?; + + let (decimals, has_permanent_delegate) = + if let Some(mint_address) = get_token_account_mint(&account_data.data) { + let mint_account = config.get_account_checked(&mint_address).await?; + let mint_state = StateWithExtensionsOwned::::unpack(mint_account.data) + .map_err(|_| format!("Could not deserialize token mint {}", mint_address))?; + + let has_permanent_delegate = + if let Ok(permanent_delegate) = mint_state.get_extension::() { + Option::::from(permanent_delegate.delegate).is_some() + } else { + false + }; + + (Some(mint_state.base.decimals), has_permanent_delegate) + } else { + (None, false) + }; + + let token_data = parse_token(&account_data.data, decimals); + + match token_data { + Ok(TokenAccountType::Account(account)) => { + let mint_address = Pubkey::from_str(&account.mint)?; + let owner = Pubkey::from_str(&account.owner)?; + let associated_address = get_associated_token_address_with_program_id( + &owner, + &mint_address, + &config.program_id, + ); + + let cli_output = CliTokenAccount { + address: address.to_string(), + program_id: config.program_id.to_string(), + is_associated: associated_address == address, + account, + has_permanent_delegate, + }; + + Ok(config.output_format.formatted_string(&cli_output)) + } + Ok(TokenAccountType::Mint(mint)) => { + let epoch_info = config.rpc_client.get_epoch_info().await?; + let cli_output = CliMint { + address: address.to_string(), + epoch: epoch_info.epoch, + program_id: config.program_id.to_string(), + mint, + }; + + Ok(config.output_format.formatted_string(&cli_output)) + } + Ok(TokenAccountType::Multisig(multisig)) => { + let cli_output = CliMultisig { + address: address.to_string(), + program_id: config.program_id.to_string(), + multisig, + }; + + Ok(config.output_format.formatted_string(&cli_output)) + } + Err(e) => Err(e.into()), + } +} + +async fn command_gc( + config: &Config<'_>, + owner: Pubkey, + close_empty_associated_accounts: bool, + bulk_signers: BulkSigners, +) -> CommandResult { + println_display( + config, + format!( + "Fetching token accounts associated with program {}", + config.program_id + ), + ); + let accounts = config + .rpc_client + .get_token_accounts_by_owner(&owner, TokenAccountsFilter::ProgramId(config.program_id)) + .await?; + if accounts.is_empty() { + println_display(config, "Nothing to do".to_string()); + return Ok("".to_string()); + } + + let mut accounts_by_token = HashMap::new(); + + for keyed_account in accounts { + if let UiAccountData::Json(parsed_account) = keyed_account.account.data { + if let Ok(TokenAccountType::Account(ui_token_account)) = + serde_json::from_value(parsed_account.parsed) + { + let frozen = ui_token_account.state == UiAccountState::Frozen; + let decimals = ui_token_account.token_amount.decimals; + + let token = ui_token_account + .mint + .parse::() + .unwrap_or_else(|err| panic!("Invalid mint: {}", err)); + let token_account = keyed_account + .pubkey + .parse::() + .unwrap_or_else(|err| panic!("Invalid token account: {}", err)); + let token_amount = ui_token_account + .token_amount + .amount + .parse::() + .unwrap_or_else(|err| panic!("Invalid token amount: {}", err)); + + let close_authority = ui_token_account.close_authority.map_or(owner, |s| { + s.parse::() + .unwrap_or_else(|err| panic!("Invalid close authority: {}", err)) + }); + + let entry = accounts_by_token + .entry((token, decimals)) + .or_insert_with(HashMap::new); + entry.insert(token_account, (token_amount, frozen, close_authority)); + } + } + } + + let mut results = vec![]; + for ((token_pubkey, decimals), accounts) in accounts_by_token.into_iter() { + println_display(config, format!("Processing token: {}", token_pubkey)); + + let token = token_client_from_config(config, &token_pubkey, Some(decimals))?; + let total_balance: u64 = accounts.values().map(|account| account.0).sum(); + + let associated_token_account = token.get_associated_token_address(&owner); + if !accounts.contains_key(&associated_token_account) && total_balance > 0 { + token.create_associated_token_account(&owner).await?; + } + + for (address, (amount, frozen, close_authority)) in accounts { + let is_associated = address == associated_token_account; + + // only close the associated account if --close-empty-associated-accounts is + // provided + if is_associated && !close_empty_associated_accounts { + continue; + } + + // never close the associated account if *any* account carries a balance + if is_associated && total_balance > 0 { + continue; + } + + // dont attempt to close frozen accounts + if frozen { + continue; + } + + if is_associated { + println!("Closing associated account {}", address); + } + + // this logic is quite fiendish, but its more readable this way than if/else + let maybe_res = match (close_authority == owner, is_associated, amount == 0) { + // owner authority, associated or auxiliary, empty -> close + (true, _, true) => Some( + token + .close_account(&address, &owner, &owner, &bulk_signers) + .await, + ), + // owner authority, auxiliary, nonempty -> empty and close + (true, false, false) => Some( + token + .empty_and_close_account( + &address, + &owner, + &associated_token_account, + &owner, + &bulk_signers, + ) + .await, + ), + // separate authority, auxiliary, nonempty -> transfer + (false, false, false) => Some( + token + .transfer( + &address, + &associated_token_account, + &owner, + amount, + &bulk_signers, + ) + .await, + ), + // separate authority, associated or auxiliary, empty -> print warning + (false, _, true) => { + println_display( + config, + format!( + "Note: skipping {} due to separate close authority {}; \ + revoke authority and rerun gc, or rerun gc with --owner", + address, close_authority + ), + ); + None + } + // anything else, including a nonempty associated account -> unreachable + (_, _, _) => unreachable!(), + }; + + if let Some(res) = maybe_res { + let tx_return = finish_tx(config, &res?, false).await?; + + results.push(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) + } + }); + }; + } + } + + Ok(results.join("")) +} + +async fn command_sync_native(config: &Config<'_>, native_account_address: Pubkey) -> CommandResult { + let token = native_token_client_from_config(config)?; + + if !config.sign_only { + let account_data = config.get_account_checked(&native_account_address).await?; + let account_state = StateWithExtensionsOwned::::unpack(account_data.data)?; + + if account_state.base.mint != *token.get_address() { + return Err(format!("{} is not a native token account", native_account_address).into()); + } + } + + let res = token.sync_native(&native_account_address).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_withdraw_excess_lamports( + config: &Config<'_>, + source_account: Pubkey, + destination_account: Pubkey, + authority: Pubkey, + bulk_signers: Vec>, +) -> CommandResult { + // default is safe here because withdraw_excess_lamports doesn't use it + let token = token_client_from_config(config, &Pubkey::default(), None)?; + println_display( + config, + format!( + "Withdrawing excess lamports\n Sender: {}\n Destination: {}", + source_account, destination_account + ), + ); + + let res = token + .withdraw_excess_lamports( + &source_account, + &destination_account, + &authority, + &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) + } + }) +} + +// both enables and disables required transfer memos, via enable_memos bool +async fn command_required_transfer_memos( + config: &Config<'_>, + token_account_address: Pubkey, + owner: Pubkey, + bulk_signers: BulkSigners, + enable_memos: bool, +) -> CommandResult { + if config.sign_only { + panic!("Config can not be sign-only for enabling/disabling required transfer memos."); + } + + let account = config.get_account_checked(&token_account_address).await?; + let current_account_len = account.data.len(); + + let state_with_extension = StateWithExtensionsOwned::::unpack(account.data)?; + let token = token_client_from_config(config, &state_with_extension.base.mint, None)?; + + // Reallocation (if needed) + let mut existing_extensions: Vec = state_with_extension.get_extension_types()?; + if existing_extensions.contains(&ExtensionType::MemoTransfer) { + let extension_state = state_with_extension + .get_extension::()? + .require_incoming_transfer_memos + .into(); + + if extension_state == enable_memos { + return Ok(format!( + "Required transfer memos were already {}", + if extension_state { + "enabled" + } else { + "disabled" + } + )); + } + } else { + existing_extensions.push(ExtensionType::MemoTransfer); + let needed_account_len = + ExtensionType::try_calculate_account_len::(&existing_extensions)?; + if needed_account_len > current_account_len { + token + .reallocate( + &token_account_address, + &owner, + &[ExtensionType::MemoTransfer], + &bulk_signers, + ) + .await?; + } + } + + let res = if enable_memos { + token + .enable_required_transfer_memos(&token_account_address, &owner, &bulk_signers) + .await + } else { + token + .disable_required_transfer_memos(&token_account_address, &owner, &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) + } + }) +} + +// both enables and disables cpi guard, via enable_guard bool +async fn command_cpi_guard( + config: &Config<'_>, + token_account_address: Pubkey, + owner: Pubkey, + bulk_signers: BulkSigners, + enable_guard: bool, +) -> CommandResult { + if config.sign_only { + panic!("Config can not be sign-only for enabling/disabling required transfer memos."); + } + + let account = config.get_account_checked(&token_account_address).await?; + let current_account_len = account.data.len(); + + let state_with_extension = StateWithExtensionsOwned::::unpack(account.data)?; + let token = token_client_from_config(config, &state_with_extension.base.mint, None)?; + + // reallocation (if needed) + let mut existing_extensions: Vec = state_with_extension.get_extension_types()?; + if existing_extensions.contains(&ExtensionType::CpiGuard) { + let extension_state = state_with_extension + .get_extension::()? + .lock_cpi + .into(); + + if extension_state == enable_guard { + return Ok(format!( + "CPI Guard was already {}", + if extension_state { + "enabled" + } else { + "disabled" + } + )); + } + } else { + existing_extensions.push(ExtensionType::CpiGuard); + let required_account_len = + ExtensionType::try_calculate_account_len::(&existing_extensions)?; + if required_account_len > current_account_len { + token + .reallocate( + &token_account_address, + &owner, + &[ExtensionType::CpiGuard], + &bulk_signers, + ) + .await?; + } + } + + let res = if enable_guard { + token + .enable_cpi_guard(&token_account_address, &owner, &bulk_signers) + .await + } else { + token + .disable_cpi_guard(&token_account_address, &owner, &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_update_metadata_pointer_address( + config: &Config<'_>, + token_pubkey: Pubkey, + authority: Pubkey, + new_metadata_address: Option, + bulk_signers: BulkSigners, +) -> CommandResult { + if config.sign_only { + panic!("Config can not be sign-only for updating metadata pointer address."); + } + + let token = token_client_from_config(config, &token_pubkey, None)?; + let res = token + .update_metadata_address(&authority, new_metadata_address, &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_update_default_account_state( + config: &Config<'_>, + token_pubkey: Pubkey, + freeze_authority: Pubkey, + new_default_state: AccountState, + bulk_signers: BulkSigners, +) -> CommandResult { + if !config.sign_only { + let mint_account = config.get_account_checked(&token_pubkey).await?; + + let mint_state = StateWithExtensionsOwned::::unpack(mint_account.data) + .map_err(|_| format!("Could not deserialize token mint {}", token_pubkey))?; + match mint_state.base.freeze_authority { + COption::None => { + return Err(format!("Mint {} has no freeze authority.", token_pubkey).into()) + } + COption::Some(mint_freeze_authority) => { + if mint_freeze_authority != freeze_authority { + return Err(format!( + "Mint {} has a freeze authority {}, {} provided", + token_pubkey, mint_freeze_authority, freeze_authority + ) + .into()); + } + } + } + + if let Ok(default_account_state) = mint_state.get_extension::() { + if default_account_state.state == u8::from(new_default_state) { + let state_string = match new_default_state { + AccountState::Frozen => "frozen", + AccountState::Initialized => "initialized", + _ => unreachable!(), + }; + return Err(format!( + "Mint {} already has default account state {}", + token_pubkey, state_string + ) + .into()); + } + } else { + return Err(format!( + "Mint {} does not support default account states", + token_pubkey + ) + .into()); + } + } + + let token = token_client_from_config(config, &token_pubkey, None)?; + let res = token + .set_default_account_state(&freeze_authority, &new_default_state, &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_withdraw_withheld_tokens( + config: &Config<'_>, + destination_token_account: Pubkey, + source_token_accounts: Vec, + authority: Pubkey, + include_mint: bool, + bulk_signers: BulkSigners, +) -> CommandResult { + if config.sign_only { + panic!("Config can not be sign-only for withdrawing withheld tokens."); + } + let destination_account = config + .get_account_checked(&destination_token_account) + .await?; + let destination_state = StateWithExtensionsOwned::::unpack(destination_account.data) + .map_err(|_| { + format!( + "Could not deserialize token account {}", + destination_token_account + ) + })?; + let token_pubkey = destination_state.base.mint; + destination_state + .get_extension::() + .map_err(|_| format!("Token mint {} has no transfer fee configured", token_pubkey))?; + + let token = token_client_from_config(config, &token_pubkey, None)?; + let mut results = vec![]; + if include_mint { + let res = token + .withdraw_withheld_tokens_from_mint( + &destination_token_account, + &authority, + &bulk_signers, + ) + .await; + let tx_return = finish_tx(config, &res?, false).await?; + results.push(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) + } + }); + } + + let source_refs = source_token_accounts.iter().collect::>(); + // this can be tweaked better, but keep it simple for now + const MAX_WITHDRAWAL_ACCOUNTS: usize = 25; + for sources in source_refs.chunks(MAX_WITHDRAWAL_ACCOUNTS) { + let res = token + .withdraw_withheld_tokens_from_accounts( + &destination_token_account, + &authority, + sources, + &bulk_signers, + ) + .await; + let tx_return = finish_tx(config, &res?, false).await?; + results.push(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) + } + }); + } + + Ok(results.join("")) +} + +async fn command_update_confidential_transfer_settings( + config: &Config<'_>, + token_pubkey: Pubkey, + authority: Pubkey, + auto_approve: Option, + auditor_pubkey: Option, + bulk_signers: Vec>, +) -> CommandResult { + let (new_auto_approve, new_auditor_pubkey) = if !config.sign_only { + let confidential_transfer_account = config.get_account_checked(&token_pubkey).await?; + + let mint_state = + StateWithExtensionsOwned::::unpack(confidential_transfer_account.data) + .map_err(|_| format!("Could not deserialize token mint {}", token_pubkey))?; + + if let Ok(confidential_transfer_mint) = + mint_state.get_extension::() + { + let expected_authority = Option::::from(confidential_transfer_mint.authority); + + if expected_authority != Some(authority) { + return Err(format!( + "Mint {} has confidential transfer authority {}, but {} was provided", + token_pubkey, + expected_authority + .map(|pubkey| pubkey.to_string()) + .unwrap_or_else(|| "disabled".to_string()), + authority + ) + .into()); + } + + let new_auto_approve = if let Some(auto_approve) = auto_approve { + auto_approve + } else { + bool::from(confidential_transfer_mint.auto_approve_new_accounts) + }; + + let new_auditor_pubkey = if let Some(auditor_pubkey) = auditor_pubkey { + auditor_pubkey.into() + } else { + Option::::from(confidential_transfer_mint.auditor_elgamal_pubkey) + }; + + (new_auto_approve, new_auditor_pubkey) + } else { + return Err(format!( + "Mint {} does not support confidential transfers", + token_pubkey + ) + .into()); + } + } else { + let new_auto_approve = auto_approve.expect("The approve policy must be provided"); + let new_auditor_pubkey = auditor_pubkey + .expect("The auditor encryption pubkey must be provided") + .into(); + + (new_auto_approve, new_auditor_pubkey) + }; + + println_display( + config, + format!( + "Updating confidential transfer settings for {}:", + token_pubkey, + ), + ); + + if auto_approve.is_some() { + println_display( + config, + format!( + " approve policy set to {}", + if new_auto_approve { "auto" } else { "manual" } + ), + ); + } + + if auditor_pubkey.is_some() { + if let Some(new_auditor_pubkey) = new_auditor_pubkey { + println_display( + config, + format!(" auditor encryption pubkey set to {}", new_auditor_pubkey,), + ); + } else { + println_display(config, " auditability disabled".to_string()) + } + } + + let token = token_client_from_config(config, &token_pubkey, None)?; + let res = token + .confidential_transfer_update_mint( + &authority, + new_auto_approve, + new_auditor_pubkey, + &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) + } + }) +} + +#[allow(clippy::too_many_arguments)] +async fn command_configure_confidential_transfer_account( + config: &Config<'_>, + maybe_token: Option, + owner: Pubkey, + maybe_account: Option, + maximum_credit_counter: Option, + elgamal_keypair: &ElGamalKeypair, + aes_key: &AeKey, + bulk_signers: BulkSigners, +) -> CommandResult { + if config.sign_only { + panic!("Sign-only is not yet supported."); + } + + let token_account_address = if let Some(account) = maybe_account { + account + } else { + let token_pubkey = + maybe_token.expect("Either a valid token or account address must be provided"); + let token = token_client_from_config(config, &token_pubkey, None)?; + token.get_associated_token_address(&owner) + }; + + let account = config.get_account_checked(&token_account_address).await?; + let current_account_len = account.data.len(); + + let state_with_extension = StateWithExtensionsOwned::::unpack(account.data)?; + let token = token_client_from_config(config, &state_with_extension.base.mint, None)?; + + // Reallocation (if needed) + let mut existing_extensions: Vec = state_with_extension.get_extension_types()?; + if !existing_extensions.contains(&ExtensionType::ConfidentialTransferAccount) { + existing_extensions.push(ExtensionType::ConfidentialTransferAccount); + let needed_account_len = + ExtensionType::try_calculate_account_len::(&existing_extensions)?; + if needed_account_len > current_account_len { + token + .reallocate( + &token_account_address, + &owner, + &[ExtensionType::ConfidentialTransferAccount], + &bulk_signers, + ) + .await?; + } + } + + let res = token + .confidential_transfer_configure_token_account( + &token_account_address, + &owner, + None, + maximum_credit_counter, + elgamal_keypair, + aes_key, + &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_enable_disable_confidential_transfers( + config: &Config<'_>, + maybe_token: Option, + owner: Pubkey, + maybe_account: Option, + bulk_signers: BulkSigners, + allow_confidential_credits: Option, + allow_non_confidential_credits: Option, +) -> CommandResult { + if config.sign_only { + panic!("Sign-only is not yet supported."); + } + + let token_account_address = if let Some(account) = maybe_account { + account + } else { + let token_pubkey = + maybe_token.expect("Either a valid token or account address must be provided"); + let token = token_client_from_config(config, &token_pubkey, None)?; + token.get_associated_token_address(&owner) + }; + + let account = config.get_account_checked(&token_account_address).await?; + + let state_with_extension = StateWithExtensionsOwned::::unpack(account.data)?; + let token = token_client_from_config(config, &state_with_extension.base.mint, None)?; + + let existing_extensions: Vec = state_with_extension.get_extension_types()?; + if !existing_extensions.contains(&ExtensionType::ConfidentialTransferAccount) { + panic!( + "Confidential transfer is not yet configured for this account. \ + Use `configure-confidential-transfer-account` command instead." + ); + } + + let res = if let Some(allow_confidential_credits) = allow_confidential_credits { + let extension_state = state_with_extension + .get_extension::()? + .allow_confidential_credits + .into(); + + if extension_state == allow_confidential_credits { + return Ok(format!( + "Confidential transfers are already {}", + if extension_state { + "enabled" + } else { + "disabled" + } + )); + } + + if allow_confidential_credits { + token + .confidential_transfer_enable_confidential_credits( + &token_account_address, + &owner, + &bulk_signers, + ) + .await + } else { + token + .confidential_transfer_disable_confidential_credits( + &token_account_address, + &owner, + &bulk_signers, + ) + .await + } + } else { + let allow_non_confidential_credits = + allow_non_confidential_credits.expect("Nothing to be done"); + let extension_state = state_with_extension + .get_extension::()? + .allow_non_confidential_credits + .into(); + + if extension_state == allow_non_confidential_credits { + return Ok(format!( + "Non-confidential transfers are already {}", + if extension_state { + "enabled" + } else { + "disabled" + } + )); + } + + if allow_non_confidential_credits { + token + .confidential_transfer_enable_non_confidential_credits( + &token_account_address, + &owner, + &bulk_signers, + ) + .await + } else { + token + .confidential_transfer_disable_non_confidential_credits( + &token_account_address, + &owner, + &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) + } + }) +} +#[derive(PartialEq, Eq)] +enum ConfidentialInstructionType { + Deposit, + Withdraw, +} + +#[allow(clippy::too_many_arguments)] +async fn command_deposit_withdraw_confidential_tokens( + config: &Config<'_>, + token_pubkey: Pubkey, + owner: Pubkey, + maybe_account: Option, + bulk_signers: BulkSigners, + ui_amount: Option, + mint_decimals: Option, + instruction_type: ConfidentialInstructionType, + elgamal_keypair: Option<&ElGamalKeypair>, + aes_key: Option<&AeKey>, +) -> CommandResult { + if config.sign_only { + panic!("Sign-only is not yet supported."); + } + + // check if mint decimals provided is consistent + let mint_info = config.get_mint_info(&token_pubkey, mint_decimals).await?; + + if !config.sign_only && mint_decimals.is_some() && mint_decimals != Some(mint_info.decimals) { + return Err(format!( + "Decimals {} was provided, but actual value is {}", + mint_decimals.unwrap(), + mint_info.decimals + ) + .into()); + } + + let decimals = if let Some(decimals) = mint_decimals { + decimals + } else { + mint_info.decimals + }; + + // derive ATA if account address not provided + let token_account_address = if let Some(account) = maybe_account { + account + } else { + let token = token_client_from_config(config, &token_pubkey, Some(decimals))?; + token.get_associated_token_address(&owner) + }; + + let account = config.get_account_checked(&token_account_address).await?; + + let state_with_extension = StateWithExtensionsOwned::::unpack(account.data)?; + let token = token_client_from_config(config, &state_with_extension.base.mint, None)?; + + // the amount the user wants to deposit or withdraw, as an f64 + let maybe_amount = + ui_amount.map(|ui_amount| spl_token::ui_amount_to_amount(ui_amount, mint_info.decimals)); + + // the amount we will deposit or withdraw, as a u64 + let amount = if !config.sign_only && instruction_type == ConfidentialInstructionType::Deposit { + let current_balance = state_with_extension.base.amount; + let deposit_amount = maybe_amount.unwrap_or(current_balance); + + println_display( + config, + format!( + "Depositing {} confidential tokens", + spl_token::amount_to_ui_amount(deposit_amount, mint_info.decimals), + ), + ); + + if deposit_amount > current_balance { + return Err(format!( + "Error: Insufficient funds, current balance is {}", + spl_token_2022::amount_to_ui_amount_string_trimmed( + current_balance, + mint_info.decimals + ) + ) + .into()); + } + + deposit_amount + } else if !config.sign_only && instruction_type == ConfidentialInstructionType::Withdraw { + // // TODO: expose account balance decryption in token + // let aes_key = aes_key.expect("AES key must be provided"); + // let current_balance = token + // .confidential_transfer_get_available_balance_with_key( + // &token_account_address, + // aes_key, + // ) + // .await?; + let withdraw_amount = + maybe_amount.expect("ALL keyword is not currently supported for withdraw"); + + println_display( + config, + format!( + "Withdrawing {} confidential tokens", + spl_token::amount_to_ui_amount(withdraw_amount, mint_info.decimals) + ), + ); + + withdraw_amount + } else { + maybe_amount.unwrap() + }; + + let res = match instruction_type { + ConfidentialInstructionType::Deposit => { + token + .confidential_transfer_deposit( + &token_account_address, + &owner, + amount, + decimals, + &bulk_signers, + ) + .await? + } + ConfidentialInstructionType::Withdraw => { + let elgamal_keypair = elgamal_keypair.expect("ElGamal keypair must be provided"); + let aes_key = aes_key.expect("AES key must be provided"); + + let extension_state = + state_with_extension.get_extension::()?; + let withdraw_account_info = WithdrawAccountInfo::new(extension_state); + + let context_state_authority = config.fee_payer()?; + let context_state_keypair = Keypair::new(); + let context_state_pubkey = context_state_keypair.pubkey(); + + let withdraw_proof_data = + withdraw_account_info.generate_proof_data(amount, elgamal_keypair, aes_key)?; + + // setup proof + token + .create_withdraw_proof_context_state( + &context_state_pubkey, + &context_state_authority.pubkey(), + &withdraw_proof_data, + &context_state_keypair, + ) + .await?; + + // do the withdrawal + token + .confidential_transfer_withdraw( + &token_account_address, + &owner, + Some(&context_state_pubkey), + amount, + decimals, + Some(withdraw_account_info), + elgamal_keypair, + aes_key, + &bulk_signers, + ) + .await?; + + // close context state account + let context_state_authority_pubkey = context_state_authority.pubkey(); + let close_context_state_signers = &[context_state_authority]; + token + .confidential_transfer_close_context_state( + &context_state_pubkey, + &token_account_address, + &context_state_authority_pubkey, + close_context_state_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) + } + }) +} + +#[allow(clippy::too_many_arguments)] +async fn command_apply_pending_balance( + config: &Config<'_>, + maybe_token: Option, + owner: Pubkey, + maybe_account: Option, + bulk_signers: BulkSigners, + elgamal_keypair: &ElGamalKeypair, + aes_key: &AeKey, +) -> CommandResult { + if config.sign_only { + panic!("Sign-only is not yet supported."); + } + + // derive ATA if account address not provided + let token_account_address = if let Some(account) = maybe_account { + account + } else { + let token_pubkey = + maybe_token.expect("Either a valid token or account address must be provided"); + let token = token_client_from_config(config, &token_pubkey, None)?; + token.get_associated_token_address(&owner) + }; + + let account = config.get_account_checked(&token_account_address).await?; + + let state_with_extension = StateWithExtensionsOwned::::unpack(account.data)?; + let token = token_client_from_config(config, &state_with_extension.base.mint, None)?; + + let extension_state = state_with_extension.get_extension::()?; + let account_info = ApplyPendingBalanceAccountInfo::new(extension_state); + + let res = token + .confidential_transfer_apply_pending_balance( + &token_account_address, + &owner, + Some(account_info), + elgamal_keypair.secret(), + aes_key, + &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) + } + }) +} + +struct ConfidentialTransferArgs { + sender_elgamal_keypair: ElGamalKeypair, + sender_aes_key: AeKey, + recipient_elgamal_pubkey: Option, + auditor_elgamal_pubkey: Option, +} + +pub async fn process_command<'a>( + sub_command: &CommandName, + sub_matches: &ArgMatches<'_>, + config: &Config<'a>, + mut wallet_manager: Option>, + mut bulk_signers: Vec>, +) -> CommandResult { + match (sub_command, sub_matches) { + (CommandName::Bench, arg_matches) => { + bench_process_command( + arg_matches, + config, + std::mem::take(&mut bulk_signers), + &mut wallet_manager, + ) + .await + } + (CommandName::CreateToken, arg_matches) => { + let decimals = value_t_or_exit!(arg_matches, "decimals", u8); + let mint_authority = + config.pubkey_or_default(arg_matches, "mint_authority", &mut wallet_manager)?; + let memo = value_t!(arg_matches, "memo", String).ok(); + let rate_bps = value_t!(arg_matches, "interest_rate", i16).ok(); + let metadata_address = value_t!(arg_matches, "metadata_address", Pubkey).ok(); + + let transfer_fee = arg_matches.values_of("transfer_fee").map(|mut v| { + ( + v.next() + .unwrap() + .parse::() + .unwrap_or_else(print_error_and_exit), + v.next() + .unwrap() + .parse::() + .unwrap_or_else(print_error_and_exit), + ) + }); + + let (token_signer, token) = + get_signer(arg_matches, "token_keypair", &mut wallet_manager) + .unwrap_or_else(new_throwaway_signer); + push_signer_with_dedup(token_signer, &mut bulk_signers); + let default_account_state = + arg_matches + .value_of("default_account_state") + .map(|s| match s { + "initialized" => AccountState::Initialized, + "frozen" => AccountState::Frozen, + _ => unreachable!(), + }); + let transfer_hook_program_id = + pubkey_of_signer(arg_matches, "transfer_hook", &mut wallet_manager).unwrap(); + + let confidential_transfer_auto_approve = arg_matches + .value_of("enable_confidential_transfers") + .map(|b| b == "auto"); + + command_create_token( + config, + decimals, + token, + mint_authority, + arg_matches.is_present("enable_freeze"), + arg_matches.is_present("enable_close"), + arg_matches.is_present("enable_non_transferable"), + arg_matches.is_present("enable_permanent_delegate"), + memo, + metadata_address, + rate_bps, + default_account_state, + transfer_fee, + confidential_transfer_auto_approve, + transfer_hook_program_id, + arg_matches.is_present("enable_metadata"), + bulk_signers, + ) + .await + } + (CommandName::SetInterestRate, arg_matches) => { + let token_pubkey = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) + .unwrap() + .unwrap(); + let rate_bps = value_t_or_exit!(arg_matches, "rate", i16); + let (rate_authority_signer, rate_authority_pubkey) = + config.signer_or_default(arg_matches, "rate_authority", &mut wallet_manager); + let bulk_signers = vec![rate_authority_signer]; + + command_set_interest_rate( + config, + token_pubkey, + rate_authority_pubkey, + rate_bps, + bulk_signers, + ) + .await + } + (CommandName::SetTransferHook, arg_matches) => { + let token_pubkey = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) + .unwrap() + .unwrap(); + let new_program_id = + pubkey_of_signer(arg_matches, "new_program_id", &mut wallet_manager).unwrap(); + let (authority_signer, authority_pubkey) = + config.signer_or_default(arg_matches, "authority", &mut wallet_manager); + let bulk_signers = vec![authority_signer]; + + command_set_transfer_hook_program( + config, + token_pubkey, + authority_pubkey, + new_program_id, + bulk_signers, + ) + .await + } + (CommandName::InitializeMetadata, arg_matches) => { + let token_pubkey = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) + .unwrap() + .unwrap(); + let name = arg_matches.value_of("name").unwrap().to_string(); + let symbol = arg_matches.value_of("symbol").unwrap().to_string(); + let uri = arg_matches.value_of("uri").unwrap().to_string(); + let (mint_authority_signer, mint_authority) = + config.signer_or_default(arg_matches, "mint_authority", &mut wallet_manager); + let bulk_signers = vec![mint_authority_signer]; + let update_authority = + config.pubkey_or_default(arg_matches, "update_authority", &mut wallet_manager)?; + + command_initialize_metadata( + config, + token_pubkey, + update_authority, + mint_authority, + name, + symbol, + uri, + bulk_signers, + ) + .await + } + (CommandName::UpdateMetadata, arg_matches) => { + let token_pubkey = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) + .unwrap() + .unwrap(); + let (authority_signer, authority) = + config.signer_or_default(arg_matches, "authority", &mut wallet_manager); + let field = arg_matches.value_of("field").unwrap(); + let field = match field.to_lowercase().as_str() { + "name" => Field::Name, + "symbol" => Field::Symbol, + "uri" => Field::Uri, + _ => Field::Key(field.to_string()), + }; + let value = arg_matches.value_of("value").map(|v| v.to_string()); + let transfer_lamports = value_of::(arg_matches, TRANSFER_LAMPORTS_ARG.name); + let bulk_signers = vec![authority_signer]; + + command_update_metadata( + config, + token_pubkey, + authority, + field, + value, + transfer_lamports, + bulk_signers, + ) + .await + } + (CommandName::CreateAccount, arg_matches) => { + let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) + .unwrap() + .unwrap(); + + // No need to add a signer when creating an associated token account + let account = get_signer(arg_matches, "account_keypair", &mut wallet_manager).map( + |(signer, account)| { + push_signer_with_dedup(signer, &mut bulk_signers); + account + }, + ); + + let owner = config.pubkey_or_default(arg_matches, "owner", &mut wallet_manager)?; + command_create_account( + config, + token, + owner, + account, + arg_matches.is_present("immutable"), + bulk_signers, + ) + .await + } + (CommandName::CreateMultisig, arg_matches) => { + let minimum_signers = value_of::(arg_matches, "minimum_signers").unwrap(); + let multisig_members = + pubkeys_of_multiple_signers(arg_matches, "multisig_member", &mut wallet_manager) + .unwrap_or_else(print_error_and_exit) + .unwrap(); + if minimum_signers as usize > multisig_members.len() { + eprintln!( + "error: MINIMUM_SIGNERS cannot be greater than the number \ + of MULTISIG_MEMBERs passed" + ); + exit(1); + } + + let (signer, _) = get_signer(arg_matches, "address_keypair", &mut wallet_manager) + .unwrap_or_else(new_throwaway_signer); + + command_create_multisig(config, signer, minimum_signers, multisig_members).await + } + (CommandName::Authorize, arg_matches) => { + let address = pubkey_of_signer(arg_matches, "address", &mut wallet_manager) + .unwrap() + .unwrap(); + let authority_type = arg_matches.value_of("authority_type").unwrap(); + let authority_type = CliAuthorityType::from_str(authority_type)?; + + let (authority_signer, authority) = + config.signer_or_default(arg_matches, "authority", &mut wallet_manager); + if config.multisigner_pubkeys.is_empty() { + push_signer_with_dedup(authority_signer, &mut bulk_signers); + } + + let new_authority = + pubkey_of_signer(arg_matches, "new_authority", &mut wallet_manager).unwrap(); + let force_authorize = arg_matches.is_present("force"); + command_authorize( + config, + address, + authority_type, + authority, + new_authority, + force_authorize, + bulk_signers, + ) + .await + } + (CommandName::Transfer, arg_matches) => { + let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) + .unwrap() + .unwrap(); + let amount = match arg_matches.value_of("amount").unwrap() { + "ALL" => None, + amount => Some(amount.parse::().unwrap()), + }; + let recipient = pubkey_of_signer(arg_matches, "recipient", &mut wallet_manager) + .unwrap() + .unwrap(); + let sender = pubkey_of_signer(arg_matches, "from", &mut wallet_manager).unwrap(); + + let (owner_signer, owner) = + config.signer_or_default(arg_matches, "owner", &mut wallet_manager); + + let confidential_transfer_args = if arg_matches.is_present("confidential") { + // Deriving ElGamal and AES key from signer. Custom ElGamal and AES keys will be + // supported in the future once upgrading to clap-v3. + // + // NOTE:: Seed bytes are hardcoded to be empty bytes for now. They will be + // updated once custom ElGamal and AES keys are supported. + let sender_elgamal_keypair = + ElGamalKeypair::new_from_signer(&*owner_signer, b"").unwrap(); + let sender_aes_key = AeKey::new_from_signer(&*owner_signer, b"").unwrap(); + + // Sign-only mode is not yet supported for confidential transfers, so set + // recipient and auditor ElGamal public to `None` by default. + Some(ConfidentialTransferArgs { + sender_elgamal_keypair, + sender_aes_key, + recipient_elgamal_pubkey: None, + auditor_elgamal_pubkey: None, + }) + } else { + None + }; + + if config.multisigner_pubkeys.is_empty() { + push_signer_with_dedup(owner_signer, &mut bulk_signers); + } + + let mint_decimals = value_of::(arg_matches, MINT_DECIMALS_ARG.name); + let fund_recipient = arg_matches.is_present("fund_recipient"); + let allow_unfunded_recipient = arg_matches.is_present("allow_empty_recipient") + || arg_matches.is_present("allow_unfunded_recipient"); + + let recipient_is_ata_owner = arg_matches.is_present("recipient_is_ata_owner"); + let no_recipient_is_ata_owner = + arg_matches.is_present("no_recipient_is_ata_owner") || !recipient_is_ata_owner; + if recipient_is_ata_owner { + println_display(config, "recipient-is-ata-owner is now the default behavior. The option has been deprecated and will be removed in a future release.".to_string()); + } + let use_unchecked_instruction = arg_matches.is_present("use_unchecked_instruction"); + let expected_fee = value_of::(arg_matches, "expected_fee"); + let memo = value_t!(arg_matches, "memo", String).ok(); + let transfer_hook_accounts = arg_matches.values_of("transfer_hook_account").map(|v| { + v.into_iter() + .map(|s| parse_transfer_hook_account(s).unwrap()) + .collect::>() + }); + + command_transfer( + config, + token, + amount, + recipient, + sender, + owner, + allow_unfunded_recipient, + fund_recipient, + mint_decimals, + no_recipient_is_ata_owner, + use_unchecked_instruction, + expected_fee, + memo, + bulk_signers, + arg_matches.is_present("no_wait"), + arg_matches.is_present("allow_non_system_account_recipient"), + transfer_hook_accounts, + confidential_transfer_args.as_ref(), + ) + .await + } + (CommandName::Burn, arg_matches) => { + let account = pubkey_of_signer(arg_matches, "account", &mut wallet_manager) + .unwrap() + .unwrap(); + + let (owner_signer, owner) = + config.signer_or_default(arg_matches, "owner", &mut wallet_manager); + if config.multisigner_pubkeys.is_empty() { + push_signer_with_dedup(owner_signer, &mut bulk_signers); + } + + let amount = value_t_or_exit!(arg_matches, "amount", f64); + let mint_address = + pubkey_of_signer(arg_matches, MINT_ADDRESS_ARG.name, &mut wallet_manager).unwrap(); + let mint_decimals = value_of::(arg_matches, MINT_DECIMALS_ARG.name); + let use_unchecked_instruction = arg_matches.is_present("use_unchecked_instruction"); + let memo = value_t!(arg_matches, "memo", String).ok(); + command_burn( + config, + account, + owner, + amount, + mint_address, + mint_decimals, + use_unchecked_instruction, + memo, + bulk_signers, + ) + .await + } + (CommandName::Mint, arg_matches) => { + let (mint_authority_signer, mint_authority) = + config.signer_or_default(arg_matches, "mint_authority", &mut wallet_manager); + if config.multisigner_pubkeys.is_empty() { + push_signer_with_dedup(mint_authority_signer, &mut bulk_signers); + } + + let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) + .unwrap() + .unwrap(); + let amount = value_t_or_exit!(arg_matches, "amount", f64); + let mint_decimals = value_of::(arg_matches, MINT_DECIMALS_ARG.name); + let mint_info = config.get_mint_info(&token, mint_decimals).await?; + let recipient = if let Some(address) = + pubkey_of_signer(arg_matches, "recipient", &mut wallet_manager).unwrap() + { + address + } else if let Some(address) = + pubkey_of_signer(arg_matches, "recipient_owner", &mut wallet_manager).unwrap() + { + get_associated_token_address_with_program_id(&address, &token, &config.program_id) + } else { + let owner = config.default_signer()?.pubkey(); + config.associated_token_address_for_token_and_program( + &mint_info.address, + &owner, + &mint_info.program_id, + )? + }; + config.check_account(&recipient, Some(token)).await?; + let use_unchecked_instruction = arg_matches.is_present("use_unchecked_instruction"); + let memo = value_t!(arg_matches, "memo", String).ok(); + command_mint( + config, + token, + amount, + recipient, + mint_info, + mint_authority, + use_unchecked_instruction, + memo, + bulk_signers, + ) + .await + } + (CommandName::Freeze, arg_matches) => { + let (freeze_authority_signer, freeze_authority) = + config.signer_or_default(arg_matches, "freeze_authority", &mut wallet_manager); + if config.multisigner_pubkeys.is_empty() { + push_signer_with_dedup(freeze_authority_signer, &mut bulk_signers); + } + + let account = pubkey_of_signer(arg_matches, "account", &mut wallet_manager) + .unwrap() + .unwrap(); + let mint_address = + pubkey_of_signer(arg_matches, MINT_ADDRESS_ARG.name, &mut wallet_manager).unwrap(); + command_freeze( + config, + account, + mint_address, + freeze_authority, + bulk_signers, + ) + .await + } + (CommandName::Thaw, arg_matches) => { + let (freeze_authority_signer, freeze_authority) = + config.signer_or_default(arg_matches, "freeze_authority", &mut wallet_manager); + if config.multisigner_pubkeys.is_empty() { + push_signer_with_dedup(freeze_authority_signer, &mut bulk_signers); + } + + let account = pubkey_of_signer(arg_matches, "account", &mut wallet_manager) + .unwrap() + .unwrap(); + let mint_address = + pubkey_of_signer(arg_matches, MINT_ADDRESS_ARG.name, &mut wallet_manager).unwrap(); + command_thaw( + config, + account, + mint_address, + freeze_authority, + bulk_signers, + ) + .await + } + (CommandName::Wrap, arg_matches) => { + let amount = value_t_or_exit!(arg_matches, "amount", f64); + let account = if arg_matches.is_present("create_aux_account") { + let (signer, account) = new_throwaway_signer(); + bulk_signers.push(signer); + Some(account) + } else { + // No need to add a signer when creating an associated token account + None + }; + + let (wallet_signer, wallet_address) = + config.signer_or_default(arg_matches, "wallet_keypair", &mut wallet_manager); + push_signer_with_dedup(wallet_signer, &mut bulk_signers); + + command_wrap( + config, + amount, + wallet_address, + account, + arg_matches.is_present("immutable"), + bulk_signers, + ) + .await + } + (CommandName::Unwrap, arg_matches) => { + let (wallet_signer, wallet_address) = + config.signer_or_default(arg_matches, "wallet_keypair", &mut wallet_manager); + push_signer_with_dedup(wallet_signer, &mut bulk_signers); + + let account = pubkey_of_signer(arg_matches, "account", &mut wallet_manager).unwrap(); + command_unwrap(config, wallet_address, account, bulk_signers).await + } + (CommandName::Approve, arg_matches) => { + let (owner_signer, owner_address) = + config.signer_or_default(arg_matches, "owner", &mut wallet_manager); + if config.multisigner_pubkeys.is_empty() { + push_signer_with_dedup(owner_signer, &mut bulk_signers); + } + + let account = pubkey_of_signer(arg_matches, "account", &mut wallet_manager) + .unwrap() + .unwrap(); + let amount = value_t_or_exit!(arg_matches, "amount", f64); + let delegate = pubkey_of_signer(arg_matches, "delegate", &mut wallet_manager) + .unwrap() + .unwrap(); + let mint_address = + pubkey_of_signer(arg_matches, MINT_ADDRESS_ARG.name, &mut wallet_manager).unwrap(); + let mint_decimals = value_of::(arg_matches, MINT_DECIMALS_ARG.name); + let use_unchecked_instruction = arg_matches.is_present("use_unchecked_instruction"); + command_approve( + config, + account, + owner_address, + amount, + delegate, + mint_address, + mint_decimals, + use_unchecked_instruction, + bulk_signers, + ) + .await + } + (CommandName::Revoke, arg_matches) => { + let (owner_signer, owner_address) = + config.signer_or_default(arg_matches, "owner", &mut wallet_manager); + if config.multisigner_pubkeys.is_empty() { + push_signer_with_dedup(owner_signer, &mut bulk_signers); + } + + let account = pubkey_of_signer(arg_matches, "account", &mut wallet_manager) + .unwrap() + .unwrap(); + let delegate_address = + pubkey_of_signer(arg_matches, DELEGATE_ADDRESS_ARG.name, &mut wallet_manager) + .unwrap(); + command_revoke( + config, + account, + owner_address, + delegate_address, + bulk_signers, + ) + .await + } + (CommandName::Close, arg_matches) => { + let (close_authority_signer, close_authority) = + config.signer_or_default(arg_matches, "close_authority", &mut wallet_manager); + if config.multisigner_pubkeys.is_empty() { + push_signer_with_dedup(close_authority_signer, &mut bulk_signers); + } + + let address = config + .associated_token_address_or_override(arg_matches, "address", &mut wallet_manager) + .await?; + let recipient = + config.pubkey_or_default(arg_matches, "recipient", &mut wallet_manager)?; + command_close(config, address, close_authority, recipient, bulk_signers).await + } + (CommandName::CloseMint, arg_matches) => { + let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) + .unwrap() + .unwrap(); + let (close_authority_signer, close_authority) = + config.signer_or_default(arg_matches, "close_authority", &mut wallet_manager); + if config.multisigner_pubkeys.is_empty() { + push_signer_with_dedup(close_authority_signer, &mut bulk_signers); + } + let recipient = + config.pubkey_or_default(arg_matches, "recipient", &mut wallet_manager)?; + + command_close_mint(config, token, close_authority, recipient, bulk_signers).await + } + (CommandName::Balance, arg_matches) => { + let address = config + .associated_token_address_or_override(arg_matches, "address", &mut wallet_manager) + .await?; + command_balance(config, address).await + } + (CommandName::Supply, arg_matches) => { + let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) + .unwrap() + .unwrap(); + command_supply(config, token).await + } + (CommandName::Accounts, arg_matches) => { + let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager).unwrap(); + let owner = config.pubkey_or_default(arg_matches, "owner", &mut wallet_manager)?; + let filter = if arg_matches.is_present("delegated") { + AccountFilter::Delegated + } else if arg_matches.is_present("externally_closeable") { + AccountFilter::ExternallyCloseable + } else { + AccountFilter::All + }; + + command_accounts( + config, + token, + owner, + filter, + arg_matches.is_present("addresses_only"), + ) + .await + } + (CommandName::Address, arg_matches) => { + let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager).unwrap(); + let owner = config.pubkey_or_default(arg_matches, "owner", &mut wallet_manager)?; + command_address(config, token, owner).await + } + (CommandName::AccountInfo, arg_matches) => { + let address = config + .associated_token_address_or_override(arg_matches, "address", &mut wallet_manager) + .await?; + command_display(config, address).await + } + (CommandName::MultisigInfo, arg_matches) => { + let address = pubkey_of_signer(arg_matches, "address", &mut wallet_manager) + .unwrap() + .unwrap(); + command_display(config, address).await + } + (CommandName::Display, arg_matches) => { + let address = pubkey_of_signer(arg_matches, "address", &mut wallet_manager) + .unwrap() + .unwrap(); + command_display(config, address).await + } + (CommandName::Gc, arg_matches) => { + match config.output_format { + OutputFormat::Json | OutputFormat::JsonCompact => { + eprintln!( + "`spl-token gc` does not support the `--ouput` parameter at this time" + ); + exit(1); + } + _ => {} + } + + let close_empty_associated_accounts = + arg_matches.is_present("close_empty_associated_accounts"); + + let (owner_signer, owner_address) = + config.signer_or_default(arg_matches, "owner", &mut wallet_manager); + if config.multisigner_pubkeys.is_empty() { + push_signer_with_dedup(owner_signer, &mut bulk_signers); + } + + command_gc( + config, + owner_address, + close_empty_associated_accounts, + bulk_signers, + ) + .await + } + (CommandName::SyncNative, arg_matches) => { + let native_mint = *native_token_client_from_config(config)?.get_address(); + let address = config + .associated_token_address_for_token_or_override( + arg_matches, + "address", + &mut wallet_manager, + Some(native_mint), + ) + .await; + command_sync_native(config, address?).await + } + (CommandName::EnableRequiredTransferMemos, arg_matches) => { + let (owner_signer, owner) = + config.signer_or_default(arg_matches, "owner", &mut wallet_manager); + if config.multisigner_pubkeys.is_empty() { + push_signer_with_dedup(owner_signer, &mut bulk_signers); + } + // Since account is required argument it will always be present + let token_account = + config.pubkey_or_default(arg_matches, "account", &mut wallet_manager)?; + command_required_transfer_memos(config, token_account, owner, bulk_signers, true).await + } + (CommandName::DisableRequiredTransferMemos, arg_matches) => { + let (owner_signer, owner) = + config.signer_or_default(arg_matches, "owner", &mut wallet_manager); + if config.multisigner_pubkeys.is_empty() { + push_signer_with_dedup(owner_signer, &mut bulk_signers); + } + // Since account is required argument it will always be present + let token_account = + config.pubkey_or_default(arg_matches, "account", &mut wallet_manager)?; + command_required_transfer_memos(config, token_account, owner, bulk_signers, false).await + } + (CommandName::EnableCpiGuard, arg_matches) => { + let (owner_signer, owner) = + config.signer_or_default(arg_matches, "owner", &mut wallet_manager); + if config.multisigner_pubkeys.is_empty() { + push_signer_with_dedup(owner_signer, &mut bulk_signers); + } + // Since account is required argument it will always be present + let token_account = + config.pubkey_or_default(arg_matches, "account", &mut wallet_manager)?; + command_cpi_guard(config, token_account, owner, bulk_signers, true).await + } + (CommandName::DisableCpiGuard, arg_matches) => { + let (owner_signer, owner) = + config.signer_or_default(arg_matches, "owner", &mut wallet_manager); + if config.multisigner_pubkeys.is_empty() { + push_signer_with_dedup(owner_signer, &mut bulk_signers); + } + // Since account is required argument it will always be present + let token_account = + config.pubkey_or_default(arg_matches, "account", &mut wallet_manager)?; + command_cpi_guard(config, token_account, owner, bulk_signers, false).await + } + (CommandName::UpdateDefaultAccountState, arg_matches) => { + // Since account is required argument it will always be present + let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) + .unwrap() + .unwrap(); + let (freeze_authority_signer, freeze_authority) = + config.signer_or_default(arg_matches, "freeze_authority", &mut wallet_manager); + if config.multisigner_pubkeys.is_empty() { + push_signer_with_dedup(freeze_authority_signer, &mut bulk_signers); + } + let new_default_state = arg_matches.value_of("state").unwrap(); + let new_default_state = match new_default_state { + "initialized" => AccountState::Initialized, + "frozen" => AccountState::Frozen, + _ => unreachable!(), + }; + command_update_default_account_state( + config, + token, + freeze_authority, + new_default_state, + bulk_signers, + ) + .await + } + (CommandName::UpdateMetadataAddress, arg_matches) => { + // Since account is required argument it will always be present + let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) + .unwrap() + .unwrap(); + + let (authority_signer, authority) = + config.signer_or_default(arg_matches, "authority", &mut wallet_manager); + if config.multisigner_pubkeys.is_empty() { + push_signer_with_dedup(authority_signer, &mut bulk_signers); + } + let metadata_address = value_t!(arg_matches, "metadata_address", Pubkey).ok(); + + command_update_metadata_pointer_address( + config, + token, + authority, + metadata_address, + bulk_signers, + ) + .await + } + (CommandName::WithdrawWithheldTokens, arg_matches) => { + let (authority_signer, authority) = config.signer_or_default( + arg_matches, + "withdraw_withheld_authority", + &mut wallet_manager, + ); + if config.multisigner_pubkeys.is_empty() { + push_signer_with_dedup(authority_signer, &mut bulk_signers); + } + // Since destination is required it will always be present + let destination_token_account = + pubkey_of_signer(arg_matches, "account", &mut wallet_manager) + .unwrap() + .unwrap(); + let include_mint = arg_matches.is_present("include_mint"); + let source_accounts = arg_matches + .values_of("source") + .unwrap_or_default() + .map(|s| Pubkey::from_str(s).unwrap_or_else(print_error_and_exit)) + .collect::>(); + command_withdraw_withheld_tokens( + config, + destination_token_account, + source_accounts, + authority, + include_mint, + bulk_signers, + ) + .await + } + (CommandName::SetTransferFee, arg_matches) => { + let token_pubkey = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) + .unwrap() + .unwrap(); + let transfer_fee_basis_points = + value_t_or_exit!(arg_matches, "transfer_fee_basis_points", u16); + let maximum_fee = value_t_or_exit!(arg_matches, "maximum_fee", f64); + let (transfer_fee_authority_signer, transfer_fee_authority_pubkey) = config + .signer_or_default(arg_matches, "transfer_fee_authority", &mut wallet_manager); + let mint_decimals = value_of::(arg_matches, MINT_DECIMALS_ARG.name); + let bulk_signers = vec![transfer_fee_authority_signer]; + + command_set_transfer_fee( + config, + token_pubkey, + transfer_fee_authority_pubkey, + transfer_fee_basis_points, + maximum_fee, + mint_decimals, + bulk_signers, + ) + .await + } + (CommandName::WithdrawExcessLamports, arg_matches) => { + let (signer, authority) = + config.signer_or_default(arg_matches, "owner", &mut wallet_manager); + if config.multisigner_pubkeys.is_empty() { + push_signer_with_dedup(signer, &mut bulk_signers); + } + + let source = config.pubkey_or_default(arg_matches, "from", &mut wallet_manager)?; + let destination = + config.pubkey_or_default(arg_matches, "recipient", &mut wallet_manager)?; + + command_withdraw_excess_lamports(config, source, destination, authority, bulk_signers) + .await + } + (CommandName::UpdateConfidentialTransferSettings, arg_matches) => { + let token_pubkey = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) + .unwrap() + .unwrap(); + + let auto_approve = arg_matches.value_of("approve_policy").map(|b| b == "auto"); + + let auditor_encryption_pubkey = if arg_matches.is_present("auditor_pubkey") { + Some(elgamal_pubkey_or_none(arg_matches, "auditor_pubkey")?) + } else { + None + }; + + let (authority_signer, authority_pubkey) = config.signer_or_default( + arg_matches, + "confidential_transfer_authority", + &mut wallet_manager, + ); + let bulk_signers = vec![authority_signer]; + + command_update_confidential_transfer_settings( + config, + token_pubkey, + authority_pubkey, + auto_approve, + auditor_encryption_pubkey, + bulk_signers, + ) + .await + } + (CommandName::ConfigureConfidentialTransferAccount, arg_matches) => { + let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager).unwrap(); + + let (owner_signer, owner) = + config.signer_or_default(arg_matches, "owner", &mut wallet_manager); + + let account = pubkey_of_signer(arg_matches, "address", &mut wallet_manager).unwrap(); + + // Deriving ElGamal and AES key from signer. Custom ElGamal and AES keys will be + // supported in the future once upgrading to clap-v3. + // + // NOTE:: Seed bytes are hardcoded to be empty bytes for now. They will be + // updated once custom ElGamal and AES keys are supported. + let elgamal_keypair = ElGamalKeypair::new_from_signer(&*owner_signer, b"").unwrap(); + let aes_key = AeKey::new_from_signer(&*owner_signer, b"").unwrap(); + + if config.multisigner_pubkeys.is_empty() { + push_signer_with_dedup(owner_signer, &mut bulk_signers); + } + + let maximum_credit_counter = + if arg_matches.is_present("maximum_pending_balance_credit_counter") { + let maximum_credit_counter = value_t_or_exit!( + arg_matches.value_of("maximum_pending_balance_credit_counter"), + u64 + ); + Some(maximum_credit_counter) + } else { + None + }; + + command_configure_confidential_transfer_account( + config, + token, + owner, + account, + maximum_credit_counter, + &elgamal_keypair, + &aes_key, + bulk_signers, + ) + .await + } + (c @ CommandName::EnableConfidentialCredits, arg_matches) + | (c @ CommandName::DisableConfidentialCredits, arg_matches) + | (c @ CommandName::EnableNonConfidentialCredits, arg_matches) + | (c @ CommandName::DisableNonConfidentialCredits, arg_matches) => { + let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager).unwrap(); + + let (owner_signer, owner) = + config.signer_or_default(arg_matches, "owner", &mut wallet_manager); + + let account = pubkey_of_signer(arg_matches, "address", &mut wallet_manager).unwrap(); + + if config.multisigner_pubkeys.is_empty() { + push_signer_with_dedup(owner_signer, &mut bulk_signers); + } + + let (allow_confidential_credits, allow_non_confidential_credits) = match c { + CommandName::EnableConfidentialCredits => (Some(true), None), + CommandName::DisableConfidentialCredits => (Some(false), None), + CommandName::EnableNonConfidentialCredits => (None, Some(true)), + CommandName::DisableNonConfidentialCredits => (None, Some(false)), + _ => (None, None), + }; + + command_enable_disable_confidential_transfers( + config, + token, + owner, + account, + bulk_signers, + allow_confidential_credits, + allow_non_confidential_credits, + ) + .await + } + (c @ CommandName::DepositConfidentialTokens, arg_matches) + | (c @ CommandName::WithdrawConfidentialTokens, arg_matches) => { + let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) + .unwrap() + .unwrap(); + let amount = match arg_matches.value_of("amount").unwrap() { + "ALL" => None, + amount => Some(amount.parse::().unwrap()), + }; + let account = pubkey_of_signer(arg_matches, "address", &mut wallet_manager).unwrap(); + + let (owner_signer, owner) = + config.signer_or_default(arg_matches, "owner", &mut wallet_manager); + + let mint_decimals = value_of::(arg_matches, MINT_DECIMALS_ARG.name); + + let (instruction_type, elgamal_keypair, aes_key) = match c { + CommandName::DepositConfidentialTokens => { + (ConfidentialInstructionType::Deposit, None, None) + } + CommandName::WithdrawConfidentialTokens => { + // Deriving ElGamal and AES key from signer. Custom ElGamal and AES keys will be + // supported in the future once upgrading to clap-v3. + // + // NOTE:: Seed bytes are hardcoded to be empty bytes for now. They will be + // updated once custom ElGamal and AES keys are supported. + let elgamal_keypair = + ElGamalKeypair::new_from_signer(&*owner_signer, b"").unwrap(); + let aes_key = AeKey::new_from_signer(&*owner_signer, b"").unwrap(); + + ( + ConfidentialInstructionType::Withdraw, + Some(elgamal_keypair), + Some(aes_key), + ) + } + _ => panic!("Instruction not supported"), + }; + + if config.multisigner_pubkeys.is_empty() { + push_signer_with_dedup(owner_signer, &mut bulk_signers); + } + + command_deposit_withdraw_confidential_tokens( + config, + token, + owner, + account, + bulk_signers, + amount, + mint_decimals, + instruction_type, + elgamal_keypair.as_ref(), + aes_key.as_ref(), + ) + .await + } + (CommandName::ApplyPendingBalance, arg_matches) => { + let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager).unwrap(); + + let (owner_signer, owner) = + config.signer_or_default(arg_matches, "owner", &mut wallet_manager); + + let account = pubkey_of_signer(arg_matches, "address", &mut wallet_manager).unwrap(); + + // Deriving ElGamal and AES key from signer. Custom ElGamal and AES keys will be + // supported in the future once upgrading to clap-v3. + // + // NOTE:: Seed bytes are hardcoded to be empty bytes for now. They will be + // updated once custom ElGamal and AES keys are supported. + let elgamal_keypair = ElGamalKeypair::new_from_signer(&*owner_signer, b"").unwrap(); + let aes_key = AeKey::new_from_signer(&*owner_signer, b"").unwrap(); + + if config.multisigner_pubkeys.is_empty() { + push_signer_with_dedup(owner_signer, &mut bulk_signers); + } + + command_apply_pending_balance( + config, + token, + owner, + account, + bulk_signers, + &elgamal_keypair, + &aes_key, + ) + .await + } + } +} + +fn format_output(command_output: T, command_name: &CommandName, config: &Config) -> String +where + T: Serialize + Display + QuietDisplay + VerboseDisplay, +{ + config.output_format.formatted_string(&CommandOutput { + command_name: command_name.to_string(), + command_output, + }) +} +enum TransactionReturnData { + CliSignature(CliSignature), + CliSignOnlyData(CliSignOnlyData), +} + +async fn finish_tx<'a>( + config: &Config<'a>, + rpc_response: &RpcClientResponse, + no_wait: bool, +) -> Result { + match rpc_response { + RpcClientResponse::Transaction(transaction) => { + Ok(TransactionReturnData::CliSignOnlyData(return_signers_data( + transaction, + &ReturnSignersConfig { + dump_transaction_message: config.dump_transaction_message, + }, + ))) + } + RpcClientResponse::Signature(signature) if no_wait => { + Ok(TransactionReturnData::CliSignature(CliSignature { + signature: signature.to_string(), + })) + } + RpcClientResponse::Signature(signature) => { + let blockhash = config.program_client.get_latest_blockhash().await?; + config + .rpc_client + .confirm_transaction_with_spinner( + signature, + &blockhash, + config.rpc_client.commitment(), + ) + .await?; + + Ok(TransactionReturnData::CliSignature(CliSignature { + signature: signature.to_string(), + })) + } + RpcClientResponse::Simulation(_) => { + // Implement this once the CLI supports dry-running / simulation + unreachable!() + } + } +} diff --git a/token/cli/src/config.rs b/token/cli/src/config.rs index f2beb0b2a58..32a3ae2dc4b 100644 --- a/token/cli/src/config.rs +++ b/token/cli/src/config.rs @@ -1,5 +1,5 @@ use { - crate::{signers_of, Error, MULTISIG_SIGNER_ARG}, + crate::clap_app::{Error, MULTISIG_SIGNER_ARG}, clap::ArgMatches, solana_clap_utils::{ input_parsers::{pubkey_of_signer, value_of}, @@ -26,31 +26,51 @@ use { std::{process::exit, rc::Rc, sync::Arc}, }; +type SignersOf = Vec<(Arc, Pubkey)>; +fn signers_of( + matches: &ArgMatches<'_>, + name: &str, + wallet_manager: &mut Option>, +) -> Result, Box> { + if let Some(values) = matches.values_of(name) { + let mut results = Vec::new(); + for (i, value) in values.enumerate() { + let name = format!("{}-{}", name, i.saturating_add(1)); + let signer = signer_from_path(matches, value, &name, wallet_manager)?; + let signer_pubkey = signer.pubkey(); + results.push((Arc::from(signer), signer_pubkey)); + } + Ok(Some(results)) + } else { + Ok(None) + } +} + pub(crate) struct MintInfo { pub program_id: Pubkey, pub address: Pubkey, pub decimals: u8, } -pub(crate) struct Config<'a> { - pub(crate) default_signer: Option>, - pub(crate) rpc_client: Arc, - pub(crate) program_client: Arc>, - pub(crate) websocket_url: String, - pub(crate) output_format: OutputFormat, - pub(crate) fee_payer: Option>, - pub(crate) nonce_account: Option, - pub(crate) nonce_authority: Option>, - pub(crate) nonce_blockhash: Option, - pub(crate) sign_only: bool, - pub(crate) dump_transaction_message: bool, - pub(crate) multisigner_pubkeys: Vec<&'a Pubkey>, - pub(crate) program_id: Pubkey, - pub(crate) restrict_to_program_id: bool, +pub struct Config<'a> { + pub default_signer: Option>, + pub rpc_client: Arc, + pub program_client: Arc>, + pub websocket_url: String, + pub output_format: OutputFormat, + pub fee_payer: Option>, + pub nonce_account: Option, + pub nonce_authority: Option>, + pub nonce_blockhash: Option, + pub sign_only: bool, + pub dump_transaction_message: bool, + pub multisigner_pubkeys: Vec<&'a Pubkey>, + pub program_id: Pubkey, + pub restrict_to_program_id: bool, } impl<'a> Config<'a> { - pub(crate) async fn new( + pub async fn new( matches: &ArgMatches<'_>, wallet_manager: &mut Option>, bulk_signers: &mut Vec>, @@ -121,7 +141,7 @@ impl<'a> Config<'a> { multisigner_ids.iter().collect::>() } - pub(crate) async fn new_with_clients_and_ws_url( + pub async fn new_with_clients_and_ws_url( matches: &ArgMatches<'_>, wallet_manager: &mut Option>, bulk_signers: &mut Vec>, @@ -290,7 +310,7 @@ impl<'a> Config<'a> { } // Returns Ok(fee payer), or Err if there is no fee payer configured - pub(crate) fn fee_payer(&self) -> Result, Error> { + pub fn fee_payer(&self) -> Result, Error> { if let Some(fee_payer) = &self.fee_payer { Ok(fee_payer.clone()) } else { diff --git a/token/cli/src/lib.rs b/token/cli/src/lib.rs new file mode 100644 index 00000000000..b38c26b44fd --- /dev/null +++ b/token/cli/src/lib.rs @@ -0,0 +1,7 @@ +mod bench; +pub mod clap_app; +pub mod command; +pub mod config; +mod encryption_keypair; +mod output; +mod sort; diff --git a/token/cli/src/main.rs b/token/cli/src/main.rs index 1a11569ad52..6566efe8d1e 100644 --- a/token/cli/src/main.rs +++ b/token/cli/src/main.rs @@ -1,10072 +1,40 @@ -#![allow(clippy::arithmetic_side_effects)] use { - clap::{ - crate_description, crate_name, crate_version, value_t, value_t_or_exit, App, AppSettings, - Arg, ArgGroup, ArgMatches, SubCommand, - }, - futures::try_join, - serde::Serialize, - solana_account_decoder::{ - parse_token::{get_token_account_mint, parse_token, TokenAccountType, UiAccountState}, - UiAccountData, - }, - solana_clap_utils::{ - fee_payer::fee_payer_arg, - input_parsers::{pubkey_of_signer, pubkeys_of_multiple_signers, value_of}, - input_validators::{ - is_amount, is_amount_or_all, is_parsable, is_pubkey, is_url_or_moniker, - is_valid_pubkey, is_valid_signer, - }, - keypair::signer_from_path, - memo::memo_arg, - nonce::*, - offline::{self, *}, - ArgConstant, - }, - solana_cli_output::{ - return_signers_data, CliSignOnlyData, CliSignature, OutputFormat, QuietDisplay, - ReturnSignersConfig, VerboseDisplay, - }, - solana_client::rpc_request::TokenAccountsFilter, - solana_remote_wallet::remote_wallet::RemoteWalletManager, - solana_sdk::{ - instruction::AccountMeta, - native_token::*, - program_option::COption, - pubkey::Pubkey, - signature::{Keypair, Signer}, - system_program, - }, - spl_associated_token_account::get_associated_token_address_with_program_id, - spl_token_2022::{ - extension::{ - confidential_transfer::{ - account_info::{ - ApplyPendingBalanceAccountInfo, TransferAccountInfo, WithdrawAccountInfo, - }, - instruction::TransferSplitContextStateAccounts, - ConfidentialTransferAccount, ConfidentialTransferMint, - }, - confidential_transfer_fee::ConfidentialTransferFeeConfig, - cpi_guard::CpiGuard, - default_account_state::DefaultAccountState, - interest_bearing_mint::InterestBearingConfig, - memo_transfer::MemoTransfer, - metadata_pointer::MetadataPointer, - mint_close_authority::MintCloseAuthority, - permanent_delegate::PermanentDelegate, - transfer_fee::{TransferFeeAmount, TransferFeeConfig}, - transfer_hook::TransferHook, - BaseStateWithExtensions, ExtensionType, StateWithExtensionsOwned, - }, - instruction::*, - solana_zk_token_sdk::{ - encryption::{ - auth_encryption::AeKey, - elgamal::{self, ElGamalKeypair}, - }, - zk_token_elgamal::pod::ElGamalPubkey, - }, - state::{Account, AccountState, Mint}, - }, - spl_token_client::{ - client::{ProgramRpcClientSendTransaction, RpcClientResponse}, - token::{ExtensionInitializationParams, Token}, - }, - spl_token_metadata_interface::state::{Field, TokenMetadata}, - std::{ - collections::HashMap, - fmt::{self, Display}, - process::exit, - rc::Rc, - str::FromStr, - sync::Arc, - }, - strum::IntoEnumIterator, - strum_macros::{EnumIter, EnumString, IntoStaticStr}, + solana_sdk::signer::Signer, + spl_token_cli::{clap_app::*, command::process_command, config::Config}, + std::{str::FromStr, sync::Arc}, }; -mod config; -use config::{Config, MintInfo}; - -mod output; -use output::*; - -mod sort; -use sort::{sort_and_parse_token_accounts, AccountFilter}; - -mod bench; -use bench::*; - -// NOTE: this submodule should be removed in the next Solana upgrade -mod encryption_keypair; -use encryption_keypair::*; - -pub const OWNER_ADDRESS_ARG: ArgConstant<'static> = ArgConstant { - name: "owner", - long: "owner", - help: "Address of the primary authority controlling a mint or account. Defaults to the client keypair address.", -}; - -pub const OWNER_KEYPAIR_ARG: ArgConstant<'static> = ArgConstant { - name: "owner", - long: "owner", - help: "Keypair of the primary authority controlling a mint or account. Defaults to the client keypair.", -}; - -pub const MINT_ADDRESS_ARG: ArgConstant<'static> = ArgConstant { - name: "mint_address", - long: "mint-address", - help: "Address of mint that token account is associated with. Required by --sign-only", -}; - -pub const MINT_DECIMALS_ARG: ArgConstant<'static> = ArgConstant { - name: "mint_decimals", - long: "mint-decimals", - help: "Decimals of mint that token account is associated with. Required by --sign-only", -}; - -pub const DELEGATE_ADDRESS_ARG: ArgConstant<'static> = ArgConstant { - name: "delegate_address", - long: "delegate-address", - help: "Address of delegate currently assigned to token account. Required by --sign-only", -}; - -pub const TRANSFER_LAMPORTS_ARG: ArgConstant<'static> = ArgConstant { - name: "transfer_lamports", - long: "transfer-lamports", - help: "Additional lamports to transfer to make account rent-exempt after reallocation. Required by --sign-only", -}; - -pub const MULTISIG_SIGNER_ARG: ArgConstant<'static> = ArgConstant { - name: "multisig_signer", - long: "multisig-signer", - help: "Member signer of a multisig account", -}; - -static VALID_TOKEN_PROGRAM_IDS: [Pubkey; 2] = [spl_token_2022::ID, spl_token::ID]; - -#[derive(Debug, Clone, Copy, PartialEq, EnumString, IntoStaticStr)] -#[strum(serialize_all = "kebab-case")] -pub enum CommandName { - CreateToken, - Close, - CloseMint, - Bench, - CreateAccount, - CreateMultisig, - Authorize, - SetInterestRate, - Transfer, - Burn, - Mint, - Freeze, - Thaw, - Wrap, - Unwrap, - Approve, - Revoke, - Balance, - Supply, - Accounts, - Address, - AccountInfo, - MultisigInfo, - Display, - Gc, - SyncNative, - EnableRequiredTransferMemos, - DisableRequiredTransferMemos, - EnableCpiGuard, - DisableCpiGuard, - UpdateDefaultAccountState, - UpdateMetadataAddress, - WithdrawWithheldTokens, - SetTransferFee, - WithdrawExcessLamports, - SetTransferHook, - InitializeMetadata, - UpdateMetadata, - UpdateConfidentialTransferSettings, - ConfigureConfidentialTransferAccount, - EnableConfidentialCredits, - DisableConfidentialCredits, - EnableNonConfidentialCredits, - DisableNonConfidentialCredits, - DepositConfidentialTokens, - WithdrawConfidentialTokens, - ApplyPendingBalance, -} -impl fmt::Display for CommandName { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:?}", self) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, EnumString, IntoStaticStr)] -#[strum(serialize_all = "kebab-case")] -pub enum AccountMetaRole { - Readonly, - Writable, - ReadonlySigner, - WritableSigner, -} -impl fmt::Display for AccountMetaRole { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:?}", self) - } -} -fn parse_transfer_hook_account(string: T) -> Result -where - T: AsRef + Display, -{ - match string.as_ref().split(':').collect::>().as_slice() { - [address, role] => { - let address = Pubkey::from_str(address).map_err(|e| format!("{e}"))?; - let meta = match AccountMetaRole::from_str(role).map_err(|e| format!("{e}"))? { - AccountMetaRole::Readonly => AccountMeta::new_readonly(address, false), - AccountMetaRole::Writable => AccountMeta::new(address, false), - AccountMetaRole::ReadonlySigner => AccountMeta::new_readonly(address, true), - AccountMetaRole::WritableSigner => AccountMeta::new(address, true), - }; - Ok(meta) - } - _ => Err("Transfer hook account must be present as
:".to_string()), - } -} -fn validate_transfer_hook_account(string: T) -> Result<(), String> -where - T: AsRef + Display, -{ - match string.as_ref().split(':').collect::>().as_slice() { - [address, role] => { - is_valid_pubkey(address)?; - AccountMetaRole::from_str(role) - .map(|_| ()) - .map_err(|e| format!("{e}")) - } - _ => Err("Transfer hook account must be present as
:".to_string()), - } -} - -#[derive(Debug, Clone, PartialEq, EnumIter, EnumString, IntoStaticStr)] -#[strum(serialize_all = "kebab-case")] -pub enum CliAuthorityType { - Mint, - Freeze, - Owner, - Close, - CloseMint, - TransferFeeConfig, - WithheldWithdraw, - InterestRate, - PermanentDelegate, - ConfidentialTransferMint, - TransferHookProgramId, - ConfidentialTransferFee, - MetadataPointer, - Metadata, -} -impl TryFrom for AuthorityType { - type Error = Error; - fn try_from(authority_type: CliAuthorityType) -> Result { - match authority_type { - CliAuthorityType::Mint => Ok(AuthorityType::MintTokens), - CliAuthorityType::Freeze => Ok(AuthorityType::FreezeAccount), - CliAuthorityType::Owner => Ok(AuthorityType::AccountOwner), - CliAuthorityType::Close => Ok(AuthorityType::CloseAccount), - CliAuthorityType::CloseMint => Ok(AuthorityType::CloseMint), - CliAuthorityType::TransferFeeConfig => Ok(AuthorityType::TransferFeeConfig), - CliAuthorityType::WithheldWithdraw => Ok(AuthorityType::WithheldWithdraw), - CliAuthorityType::InterestRate => Ok(AuthorityType::InterestRate), - CliAuthorityType::PermanentDelegate => Ok(AuthorityType::PermanentDelegate), - CliAuthorityType::ConfidentialTransferMint => { - Ok(AuthorityType::ConfidentialTransferMint) - } - CliAuthorityType::TransferHookProgramId => Ok(AuthorityType::TransferHookProgramId), - CliAuthorityType::ConfidentialTransferFee => { - Ok(AuthorityType::ConfidentialTransferFeeConfig) - } - CliAuthorityType::MetadataPointer => Ok(AuthorityType::MetadataPointer), - CliAuthorityType::Metadata => { - Err("Metadata authority does not map to a token authority type".into()) - } - } - } -} - -pub fn owner_address_arg<'a, 'b>() -> Arg<'a, 'b> { - Arg::with_name(OWNER_ADDRESS_ARG.name) - .long(OWNER_ADDRESS_ARG.long) - .takes_value(true) - .value_name("OWNER_ADDRESS") - .validator(is_valid_pubkey) - .help(OWNER_ADDRESS_ARG.help) -} - -pub fn owner_keypair_arg_with_value_name<'a, 'b>(value_name: &'static str) -> Arg<'a, 'b> { - Arg::with_name(OWNER_KEYPAIR_ARG.name) - .long(OWNER_KEYPAIR_ARG.long) - .takes_value(true) - .value_name(value_name) - .validator(is_valid_signer) - .help(OWNER_KEYPAIR_ARG.help) -} - -pub fn owner_keypair_arg<'a, 'b>() -> Arg<'a, 'b> { - owner_keypair_arg_with_value_name("OWNER_KEYPAIR") -} - -pub fn mint_address_arg<'a, 'b>() -> Arg<'a, 'b> { - Arg::with_name(MINT_ADDRESS_ARG.name) - .long(MINT_ADDRESS_ARG.long) - .takes_value(true) - .value_name("MINT_ADDRESS") - .validator(is_valid_pubkey) - .help(MINT_ADDRESS_ARG.help) -} - -fn is_mint_decimals(string: String) -> Result<(), String> { - is_parsable::(string) -} - -pub fn mint_decimals_arg<'a, 'b>() -> Arg<'a, 'b> { - Arg::with_name(MINT_DECIMALS_ARG.name) - .long(MINT_DECIMALS_ARG.long) - .takes_value(true) - .value_name("MINT_DECIMALS") - .validator(is_mint_decimals) - .help(MINT_DECIMALS_ARG.help) -} - -pub trait MintArgs { - fn mint_args(self) -> Self; -} - -impl MintArgs for App<'_, '_> { - fn mint_args(self) -> Self { - self.arg(mint_address_arg().requires(MINT_DECIMALS_ARG.name)) - .arg(mint_decimals_arg().requires(MINT_ADDRESS_ARG.name)) - } -} - -pub fn delegate_address_arg<'a, 'b>() -> Arg<'a, 'b> { - Arg::with_name(DELEGATE_ADDRESS_ARG.name) - .long(DELEGATE_ADDRESS_ARG.long) - .takes_value(true) - .value_name("DELEGATE_ADDRESS") - .validator(is_valid_pubkey) - .help(DELEGATE_ADDRESS_ARG.help) -} - -pub fn transfer_lamports_arg<'a, 'b>() -> Arg<'a, 'b> { - Arg::with_name(TRANSFER_LAMPORTS_ARG.name) - .long(TRANSFER_LAMPORTS_ARG.long) - .takes_value(true) - .value_name("LAMPORTS") - .validator(is_amount) - .help(TRANSFER_LAMPORTS_ARG.help) -} - -pub fn multisig_signer_arg<'a, 'b>() -> Arg<'a, 'b> { - Arg::with_name(MULTISIG_SIGNER_ARG.name) - .long(MULTISIG_SIGNER_ARG.long) - .validator(is_valid_signer) - .value_name("MULTISIG_SIGNER") - .takes_value(true) - .multiple(true) - .min_values(0u64) - .max_values(MAX_SIGNERS as u64) - .help(MULTISIG_SIGNER_ARG.help) -} - -fn is_multisig_minimum_signers(string: String) -> Result<(), String> { - let v = u8::from_str(&string).map_err(|e| e.to_string())? as usize; - if v < MIN_SIGNERS { - Err(format!("must be at least {}", MIN_SIGNERS)) - } else if v > MAX_SIGNERS { - Err(format!("must be at most {}", MAX_SIGNERS)) - } else { - Ok(()) - } -} - -fn is_valid_token_program_id(string: T) -> Result<(), String> -where - T: AsRef + Display, -{ - match is_pubkey(string.as_ref()) { - Ok(()) => { - let program_id = string.as_ref().parse::().unwrap(); - if VALID_TOKEN_PROGRAM_IDS.contains(&program_id) { - Ok(()) - } else { - Err(format!("Unrecognized token program id: {}", program_id)) - } - } - Err(e) => Err(e), - } -} - -pub(crate) type Error = Box; -fn print_error_and_exit(e: E) -> T { - eprintln!("error: {}", e); - exit(1) -} - -type BulkSigners = Vec>; -pub(crate) type CommandResult = Result; - -fn push_signer_with_dedup(signer: Arc, bulk_signers: &mut BulkSigners) { - if !bulk_signers.contains(&signer) { - bulk_signers.push(signer); - } -} - -fn new_throwaway_signer() -> (Arc, Pubkey) { - let keypair = Keypair::new(); - let pubkey = keypair.pubkey(); - (Arc::new(keypair) as Arc, pubkey) -} - -fn get_signer( - matches: &ArgMatches<'_>, - keypair_name: &str, - wallet_manager: &mut Option>, -) -> Option<(Arc, Pubkey)> { - matches.value_of(keypair_name).map(|path| { - let signer = signer_from_path(matches, path, keypair_name, wallet_manager) - .unwrap_or_else(print_error_and_exit); - let signer_pubkey = signer.pubkey(); - (Arc::from(signer), signer_pubkey) - }) -} - -pub(crate) async fn check_fee_payer_balance( - config: &Config<'_>, - required_balance: u64, -) -> Result<(), Error> { - let balance = config - .rpc_client - .get_balance(&config.fee_payer()?.pubkey()) - .await?; - if balance < required_balance { - Err(format!( - "Fee payer, {}, has insufficient balance: {} required, {} available", - config.fee_payer()?.pubkey(), - lamports_to_sol(required_balance), - lamports_to_sol(balance) - ) - .into()) - } else { - Ok(()) - } -} - -async fn check_wallet_balance( - config: &Config<'_>, - wallet: &Pubkey, - required_balance: u64, -) -> Result<(), Error> { - let balance = config.rpc_client.get_balance(wallet).await?; - if balance < required_balance { - Err(format!( - "Wallet {}, has insufficient balance: {} required, {} available", - wallet, - lamports_to_sol(required_balance), - lamports_to_sol(balance) - ) - .into()) - } else { - Ok(()) - } -} - -type SignersOf = Vec<(Arc, Pubkey)>; -pub fn signers_of( - matches: &ArgMatches<'_>, - name: &str, - wallet_manager: &mut Option>, -) -> Result, Box> { - if let Some(values) = matches.values_of(name) { - let mut results = Vec::new(); - for (i, value) in values.enumerate() { - let name = format!("{}-{}", name, i + 1); - let signer = signer_from_path(matches, value, &name, wallet_manager)?; - let signer_pubkey = signer.pubkey(); - results.push((Arc::from(signer), signer_pubkey)); - } - Ok(Some(results)) - } else { - Ok(None) - } -} - -fn token_client_from_config( - config: &Config<'_>, - token_pubkey: &Pubkey, - decimals: Option, -) -> Result, Error> { - let token = Token::new( - config.program_client.clone(), - &config.program_id, - token_pubkey, - decimals, - config.fee_payer()?.clone(), - ); - - if let (Some(nonce_account), Some(nonce_authority), Some(nonce_blockhash)) = ( - config.nonce_account, - &config.nonce_authority, - config.nonce_blockhash, - ) { - Ok(token.with_nonce( - &nonce_account, - Arc::clone(nonce_authority), - &nonce_blockhash, - )) - } else { - Ok(token) - } -} - -fn native_token_client_from_config( - config: &Config<'_>, -) -> Result, Error> { - let token = Token::new_native( - config.program_client.clone(), - &config.program_id, - config.fee_payer()?.clone(), - ); - - if let (Some(nonce_account), Some(nonce_authority), Some(nonce_blockhash)) = ( - config.nonce_account, - &config.nonce_authority, - config.nonce_blockhash, - ) { - Ok(token.with_nonce( - &nonce_account, - Arc::clone(nonce_authority), - &nonce_blockhash, - )) - } else { - Ok(token) - } -} - -#[allow(clippy::too_many_arguments)] -async fn command_create_token( - config: &Config<'_>, - decimals: u8, - token_pubkey: Pubkey, - authority: Pubkey, - enable_freeze: bool, - enable_close: bool, - enable_non_transferable: bool, - enable_permanent_delegate: bool, - memo: Option, - metadata_address: Option, - rate_bps: Option, - default_account_state: Option, - transfer_fee: Option<(u16, u64)>, - confidential_transfer_auto_approve: Option, - transfer_hook_program_id: Option, - enable_metadata: bool, - bulk_signers: Vec>, -) -> CommandResult { - println_display( - config, - format!( - "Creating token {} under program {}", - token_pubkey, config.program_id - ), - ); - - let token = token_client_from_config(config, &token_pubkey, Some(decimals))?; - - let freeze_authority = if enable_freeze { Some(authority) } else { None }; - - let mut extensions = vec![]; - - if enable_close { - extensions.push(ExtensionInitializationParams::MintCloseAuthority { - close_authority: Some(authority), - }); - } - - if enable_permanent_delegate { - extensions.push(ExtensionInitializationParams::PermanentDelegate { - delegate: authority, - }); - } - - if let Some(rate_bps) = rate_bps { - extensions.push(ExtensionInitializationParams::InterestBearingConfig { - rate_authority: Some(authority), - rate: rate_bps, - }) - } - - if enable_non_transferable { - extensions.push(ExtensionInitializationParams::NonTransferable); - } - - if let Some(state) = default_account_state { - assert!( - enable_freeze, - "Token requires a freeze authority to default to frozen accounts" - ); - extensions.push(ExtensionInitializationParams::DefaultAccountState { state }) - } - - if let Some((transfer_fee_basis_points, maximum_fee)) = transfer_fee { - extensions.push(ExtensionInitializationParams::TransferFeeConfig { - transfer_fee_config_authority: Some(authority), - withdraw_withheld_authority: Some(authority), - transfer_fee_basis_points, - maximum_fee, - }); - } - - if let Some(auto_approve) = confidential_transfer_auto_approve { - extensions.push(ExtensionInitializationParams::ConfidentialTransferMint { - authority: Some(authority), - auto_approve_new_accounts: auto_approve, - auditor_elgamal_pubkey: None, - }); - } - - if let Some(program_id) = transfer_hook_program_id { - extensions.push(ExtensionInitializationParams::TransferHook { - authority: Some(authority), - program_id: Some(program_id), - }); - } - - if let Some(text) = memo { - token.with_memo(text, vec![config.default_signer()?.pubkey()]); - } - - // CLI checks that only one is set - if metadata_address.is_some() || enable_metadata { - let metadata_address = if enable_metadata { - Some(token_pubkey) - } else { - metadata_address - }; - extensions.push(ExtensionInitializationParams::MetadataPointer { - authority: Some(authority), - metadata_address, - }); - } - - let res = token - .create_mint( - &authority, - freeze_authority.as_ref(), - extensions, - &bulk_signers, - ) - .await?; - - let tx_return = finish_tx(config, &res, false).await?; - - if enable_metadata { - println_display( - config, - format!( - "To initialize metadata inside the mint, please run \ - `spl-token initialize-metadata {token_pubkey} `, \ - and sign with the mint authority.", - ), - ); - } - - Ok(match tx_return { - TransactionReturnData::CliSignature(cli_signature) => format_output( - CliCreateToken { - address: token_pubkey.to_string(), - decimals, - transaction_data: cli_signature, - }, - &CommandName::CreateToken, - config, - ), - TransactionReturnData::CliSignOnlyData(cli_sign_only_data) => { - format_output(cli_sign_only_data, &CommandName::CreateToken, config) - } - }) -} - -async fn command_set_interest_rate( - config: &Config<'_>, - token_pubkey: Pubkey, - rate_authority: Pubkey, - rate_bps: i16, - bulk_signers: Vec>, -) -> CommandResult { - let token = token_client_from_config(config, &token_pubkey, None)?; - - if !config.sign_only { - let mint_account = config.get_account_checked(&token_pubkey).await?; - - let mint_state = StateWithExtensionsOwned::::unpack(mint_account.data) - .map_err(|_| format!("Could not deserialize token mint {}", token_pubkey))?; - - if let Ok(interest_rate_config) = mint_state.get_extension::() { - let mint_rate_authority_pubkey = - Option::::from(interest_rate_config.rate_authority); - - if mint_rate_authority_pubkey != Some(rate_authority) { - return Err(format!( - "Mint {} has interest rate authority {}, but {} was provided", - token_pubkey, - mint_rate_authority_pubkey - .map(|pubkey| pubkey.to_string()) - .unwrap_or_else(|| "disabled".to_string()), - rate_authority - ) - .into()); - } - } else { - return Err(format!("Mint {} is not interest-bearing", token_pubkey).into()); - } - } - - println_display( - config, - format!( - "Setting Interest Rate for {} to {} bps", - token_pubkey, rate_bps - ), - ); - - let res = token - .update_interest_rate(&rate_authority, rate_bps, &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_set_transfer_hook_program( - config: &Config<'_>, - token_pubkey: Pubkey, - authority: Pubkey, - new_program_id: Option, - bulk_signers: Vec>, -) -> CommandResult { - let token = token_client_from_config(config, &token_pubkey, None)?; - - if !config.sign_only { - let mint_account = config.get_account_checked(&token_pubkey).await?; - - let mint_state = StateWithExtensionsOwned::::unpack(mint_account.data) - .map_err(|_| format!("Could not deserialize token mint {}", token_pubkey))?; - - if let Ok(extension) = mint_state.get_extension::() { - let authority_pubkey = Option::::from(extension.authority); - - if authority_pubkey != Some(authority) { - return Err(format!( - "Mint {} has transfer hook authority {}, but {} was provided", - token_pubkey, - authority_pubkey - .map(|pubkey| pubkey.to_string()) - .unwrap_or_else(|| "disabled".to_string()), - authority - ) - .into()); - } - } else { - return Err( - format!("Mint {} does not have permissioned-transfers", token_pubkey).into(), - ); - } - } - - println_display( - config, - format!( - "Setting Transfer Hook Program id for {} to {}", - token_pubkey, - new_program_id - .map(|pubkey| pubkey.to_string()) - .unwrap_or_else(|| "disabled".to_string()) - ), - ); - - let res = token - .update_transfer_hook_program_id(&authority, new_program_id, &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) - } - }) -} - -#[allow(clippy::too_many_arguments)] -async fn command_initialize_metadata( - config: &Config<'_>, - token_pubkey: Pubkey, - update_authority: Pubkey, - mint_authority: Pubkey, - name: String, - symbol: String, - uri: String, - bulk_signers: Vec>, -) -> CommandResult { - let token = token_client_from_config(config, &token_pubkey, None)?; - - let res = token - .token_metadata_initialize_with_rent_transfer( - &config.fee_payer()?.pubkey(), - &update_authority, - &mint_authority, - name, - symbol, - uri, - &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_update_metadata( - config: &Config<'_>, - token_pubkey: Pubkey, - authority: Pubkey, - field: Field, - value: Option, - transfer_lamports: Option, - bulk_signers: Vec>, -) -> CommandResult { - let token = token_client_from_config(config, &token_pubkey, None)?; - - let res = if let Some(value) = value { - token - .token_metadata_update_field_with_rent_transfer( - &config.fee_payer()?.pubkey(), - &authority, - field, - value, - transfer_lamports, - &bulk_signers, - ) - .await? - } else if let Field::Key(key) = field { - token - .token_metadata_remove_key( - &authority, - key, - true, // idempotent - &bulk_signers, - ) - .await? - } else { - return Err(format!( - "Attempting to remove field {field:?}, which cannot be removed. \ - Please re-run the command with a value of \"\" rather than the `--remove` flag." - ) - .into()); - }; - - 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_set_transfer_fee( - config: &Config<'_>, - token_pubkey: Pubkey, - transfer_fee_authority: Pubkey, - transfer_fee_basis_points: u16, - maximum_fee: f64, - mint_decimals: Option, - bulk_signers: Vec>, -) -> CommandResult { - let decimals = if !config.sign_only { - let mint_account = config.get_account_checked(&token_pubkey).await?; - - let mint_state = StateWithExtensionsOwned::::unpack(mint_account.data) - .map_err(|_| format!("Could not deserialize token mint {}", token_pubkey))?; - - if mint_decimals.is_some() && mint_decimals != Some(mint_state.base.decimals) { - return Err(format!( - "Decimals {} was provided, but actual value is {}", - mint_decimals.unwrap(), - mint_state.base.decimals - ) - .into()); - } - - if let Ok(transfer_fee_config) = mint_state.get_extension::() { - let mint_fee_authority_pubkey = - Option::::from(transfer_fee_config.transfer_fee_config_authority); - - if mint_fee_authority_pubkey != Some(transfer_fee_authority) { - return Err(format!( - "Mint {} has transfer fee authority {}, but {} was provided", - token_pubkey, - mint_fee_authority_pubkey - .map(|pubkey| pubkey.to_string()) - .unwrap_or_else(|| "disabled".to_string()), - transfer_fee_authority - ) - .into()); - } - } else { - return Err(format!("Mint {} does not have a transfer fee", token_pubkey).into()); - } - mint_state.base.decimals - } else { - mint_decimals.unwrap() - }; - - println_display( - config, - format!( - "Setting transfer fee for {} to {} bps, {} maximum", - token_pubkey, transfer_fee_basis_points, maximum_fee - ), - ); - - let token = token_client_from_config(config, &token_pubkey, Some(decimals))?; - let maximum_fee = spl_token::ui_amount_to_amount(maximum_fee, decimals); - let res = token - .set_transfer_fee( - &transfer_fee_authority, - transfer_fee_basis_points, - maximum_fee, - &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_create_account( - config: &Config<'_>, - token_pubkey: Pubkey, - owner: Pubkey, - maybe_account: Option, - immutable_owner: bool, - bulk_signers: Vec>, -) -> CommandResult { - let token = token_client_from_config(config, &token_pubkey, None)?; - let mut extensions = vec![]; - - let (account, is_associated) = if let Some(account) = maybe_account { - ( - account, - token.get_associated_token_address(&owner) == account, - ) - } else { - (token.get_associated_token_address(&owner), true) - }; - - println_display(config, format!("Creating account {}", account)); - - if !config.sign_only { - if let Some(account_data) = config.program_client.get_account(account).await? { - if account_data.owner != system_program::id() || !is_associated { - return Err(format!("Error: Account already exists: {}", account).into()); - } - } - } - - if immutable_owner { - if config.program_id == spl_token::id() { - return Err(format!( - "Specified --immutable, but token program {} does not support the extension", - config.program_id - ) - .into()); - } else if is_associated { - println_display( - config, - "Note: --immutable specified, but Token-2022 ATAs are always immutable, ignoring" - .to_string(), - ); - } else { - extensions.push(ExtensionType::ImmutableOwner); - } - } - - let res = if is_associated { - token.create_associated_token_account(&owner).await - } else { - let signer = bulk_signers - .iter() - .find(|signer| signer.pubkey() == account) - .unwrap_or_else(|| panic!("No signer provided for account {}", account)); - - token - .create_auxiliary_token_account_with_extension_space(&**signer, &owner, extensions) - .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_create_multisig( - config: &Config<'_>, - multisig: Arc, - minimum_signers: u8, - multisig_members: Vec, -) -> CommandResult { - println_display( - config, - format!( - "Creating {}/{} multisig {} under program {}", - minimum_signers, - multisig_members.len(), - multisig.pubkey(), - config.program_id, - ), - ); - - // default is safe here because create_multisig doesnt use it - let token = token_client_from_config(config, &Pubkey::default(), None)?; - - let res = token - .create_multisig( - &*multisig, - &multisig_members.iter().collect::>(), - minimum_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) - } - }) -} - -#[allow(clippy::too_many_arguments)] -async fn command_authorize( - config: &Config<'_>, - account: Pubkey, - authority_type: CliAuthorityType, - authority: Pubkey, - new_authority: Option, - force_authorize: bool, - bulk_signers: BulkSigners, -) -> CommandResult { - let auth_str: &'static str = (&authority_type).into(); - - let (mint_pubkey, previous_authority) = if !config.sign_only { - let target_account = config.get_account_checked(&account).await?; - - let (mint_pubkey, previous_authority) = if let Ok(mint) = - StateWithExtensionsOwned::::unpack(target_account.data.clone()) - { - let previous_authority = match authority_type { - CliAuthorityType::Owner | CliAuthorityType::Close => Err(format!( - "Authority type `{}` not supported for SPL Token mints", - auth_str - )), - CliAuthorityType::Mint => Ok(Option::::from(mint.base.mint_authority)), - CliAuthorityType::Freeze => Ok(Option::::from(mint.base.freeze_authority)), - CliAuthorityType::CloseMint => { - if let Ok(mint_close_authority) = mint.get_extension::() { - Ok(Option::::from(mint_close_authority.close_authority)) - } else { - Err(format!( - "Mint `{}` does not support close authority", - account - )) - } - } - CliAuthorityType::TransferFeeConfig => { - if let Ok(transfer_fee_config) = mint.get_extension::() { - Ok(Option::::from( - transfer_fee_config.transfer_fee_config_authority, - )) - } else { - Err(format!("Mint `{}` does not support transfer fees", account)) - } - } - CliAuthorityType::WithheldWithdraw => { - if let Ok(transfer_fee_config) = mint.get_extension::() { - Ok(Option::::from( - transfer_fee_config.withdraw_withheld_authority, - )) - } else { - Err(format!("Mint `{}` does not support transfer fees", account)) - } - } - CliAuthorityType::InterestRate => { - if let Ok(interest_rate_config) = mint.get_extension::() - { - Ok(Option::::from(interest_rate_config.rate_authority)) - } else { - Err(format!("Mint `{}` is not interest-bearing", account)) - } - } - CliAuthorityType::PermanentDelegate => { - if let Ok(permanent_delegate) = mint.get_extension::() { - Ok(Option::::from(permanent_delegate.delegate)) - } else { - Err(format!( - "Mint `{}` does not support permanent delegate", - account - )) - } - } - CliAuthorityType::ConfidentialTransferMint => { - if let Ok(confidential_transfer_mint) = - mint.get_extension::() - { - Ok(Option::::from(confidential_transfer_mint.authority)) - } else { - Err(format!( - "Mint `{}` does not support confidential transfers", - account - )) - } - } - CliAuthorityType::TransferHookProgramId => { - if let Ok(extension) = mint.get_extension::() { - Ok(Option::::from(extension.authority)) - } else { - Err(format!( - "Mint `{}` does not support a transfer hook program", - account - )) - } - } - CliAuthorityType::ConfidentialTransferFee => { - if let Ok(confidential_transfer_fee_config) = - mint.get_extension::() - { - Ok(Option::::from( - confidential_transfer_fee_config.authority, - )) - } else { - Err(format!( - "Mint `{}` does not support confidential transfer fees", - account - )) - } - } - CliAuthorityType::MetadataPointer => { - if let Ok(extension) = mint.get_extension::() { - Ok(Option::::from(extension.authority)) - } else { - Err(format!( - "Mint `{}` does not support a metadata pointer", - account - )) - } - } - CliAuthorityType::Metadata => { - if let Ok(extension) = mint.get_variable_len_extension::() { - Ok(Option::::from(extension.update_authority)) - } else { - Err(format!("Mint `{account}` does not support metadata")) - } - } - }?; - - Ok((account, previous_authority)) - } else if let Ok(token_account) = - StateWithExtensionsOwned::::unpack(target_account.data) - { - let check_associated_token_account = || -> Result<(), Error> { - let maybe_associated_token_account = get_associated_token_address_with_program_id( - &token_account.base.owner, - &token_account.base.mint, - &config.program_id, - ); - if account == maybe_associated_token_account - && !force_authorize - && Some(authority) != new_authority - { - Err(format!( - "Error: attempting to change the `{}` of an associated token account", - auth_str - ) - .into()) - } else { - Ok(()) - } - }; - - let previous_authority = match authority_type { - CliAuthorityType::Mint - | CliAuthorityType::Freeze - | CliAuthorityType::CloseMint - | CliAuthorityType::TransferFeeConfig - | CliAuthorityType::WithheldWithdraw - | CliAuthorityType::InterestRate - | CliAuthorityType::PermanentDelegate - | CliAuthorityType::ConfidentialTransferMint - | CliAuthorityType::TransferHookProgramId - | CliAuthorityType::ConfidentialTransferFee - | CliAuthorityType::MetadataPointer - | CliAuthorityType::Metadata => Err(format!( - "Authority type `{auth_str}` not supported for SPL Token accounts", - )), - CliAuthorityType::Owner => { - check_associated_token_account()?; - Ok(Some(token_account.base.owner)) - } - CliAuthorityType::Close => { - check_associated_token_account()?; - Ok(Some( - token_account - .base - .close_authority - .unwrap_or(token_account.base.owner), - )) - } - }?; - - Ok((token_account.base.mint, previous_authority)) - } else { - Err("Unsupported account data format".to_string()) - }?; - - (mint_pubkey, previous_authority) - } else { - // default is safe here because authorize doesnt use it - (Pubkey::default(), None) - }; - - let token = token_client_from_config(config, &mint_pubkey, None)?; - - println_display( - config, - format!( - "Updating {}\n Current {}: {}\n New {}: {}", - account, - auth_str, - previous_authority - .map(|pubkey| pubkey.to_string()) - .unwrap_or_else(|| if config.sign_only { - "unknown".to_string() - } else { - "disabled".to_string() - }), - auth_str, - new_authority - .map(|pubkey| pubkey.to_string()) - .unwrap_or_else(|| "disabled".to_string()) - ), - ); - - let res = if let CliAuthorityType::Metadata = authority_type { - token - .token_metadata_update_authority(&authority, new_authority, &bulk_signers) - .await? - } else { - token - .set_authority( - &account, - &authority, - new_authority.as_ref(), - authority_type.try_into()?, - &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) - } - }) -} - -#[allow(clippy::too_many_arguments)] -async fn command_transfer( - config: &Config<'_>, - token_pubkey: Pubkey, - ui_amount: Option, - recipient: Pubkey, - sender: Option, - sender_owner: Pubkey, - allow_unfunded_recipient: bool, - fund_recipient: bool, - mint_decimals: Option, - no_recipient_is_ata_owner: bool, - use_unchecked_instruction: bool, - ui_fee: Option, - memo: Option, - bulk_signers: BulkSigners, - no_wait: bool, - allow_non_system_account_recipient: bool, - transfer_hook_accounts: Option>, - confidential_transfer_args: Option<&ConfidentialTransferArgs>, -) -> CommandResult { - let mint_info = config.get_mint_info(&token_pubkey, mint_decimals).await?; - - // if the user got the decimals wrong, they may well have calculated the - // transfer amount wrong we only check in online mode, because in offline, - // mint_info.decimals is always 9 - if !config.sign_only && mint_decimals.is_some() && mint_decimals != Some(mint_info.decimals) { - return Err(format!( - "Decimals {} was provided, but actual value is {}", - mint_decimals.unwrap(), - mint_info.decimals - ) - .into()); - } - - // decimals determines whether transfer_checked is used or not - // in online mode, mint_decimals may be None but mint_info.decimals is always - // correct in offline mode, mint_info.decimals may be wrong, but - // mint_decimals is always provided and in online mode, when mint_decimals - // is provided, it is verified correct hence the fallthrough logic here - let decimals = if use_unchecked_instruction { - None - } else if mint_decimals.is_some() { - mint_decimals - } else { - Some(mint_info.decimals) - }; - - let token = if let Some(transfer_hook_accounts) = transfer_hook_accounts { - token_client_from_config(config, &token_pubkey, decimals)? - .with_transfer_hook_accounts(transfer_hook_accounts) - } else { - token_client_from_config(config, &token_pubkey, decimals)? - }; - - // pubkey of the actual account we are sending from - let sender = if let Some(sender) = sender { - sender - } else { - token.get_associated_token_address(&sender_owner) - }; - - // the amount the user wants to tranfer, as a f64 - let maybe_transfer_balance = - ui_amount.map(|ui_amount| spl_token::ui_amount_to_amount(ui_amount, mint_info.decimals)); - - // the amount we will transfer, as a u64 - let transfer_balance = if !config.sign_only { - let sender_balance = token.get_account_info(&sender).await?.base.amount; - let transfer_balance = maybe_transfer_balance.unwrap_or(sender_balance); - - println_display( - config, - format!( - "{}Transfer {} tokens\n Sender: {}\n Recipient: {}", - if confidential_transfer_args.is_some() { - "Confidential " - } else { - "" - }, - spl_token::amount_to_ui_amount(transfer_balance, mint_info.decimals), - sender, - recipient - ), - ); - - if transfer_balance > sender_balance && confidential_transfer_args.is_none() { - return Err(format!( - "Error: Sender has insufficient funds, current balance is {}", - spl_token_2022::amount_to_ui_amount_string_trimmed( - sender_balance, - mint_info.decimals - ) - ) - .into()); - } - - transfer_balance - } else { - maybe_transfer_balance.unwrap() - }; - - let maybe_fee = - ui_fee.map(|ui_amount| spl_token::ui_amount_to_amount(ui_amount, mint_info.decimals)); - - // determine whether recipient is a token account or an expected owner of one - let recipient_is_token_account = if !config.sign_only { - // in online mode we can fetch it and see - let maybe_recipient_account_data = config.program_client.get_account(recipient).await?; - - // if the account exists, and: - // * its a token for this program, we are happy - // * its a system account, we are happy - // * its a non-account for this program, we error helpfully - // * its a token account for a different program, we error helpfully - // * otherwise its probabaly a program account owner of an ata, in which case we - // gate transfer with a flag - if let Some(recipient_account_data) = maybe_recipient_account_data { - let recipient_account_owner = recipient_account_data.owner; - let maybe_account_state = - StateWithExtensionsOwned::::unpack(recipient_account_data.data); - - if recipient_account_owner == config.program_id && maybe_account_state.is_ok() { - if let Ok(memo_transfer) = maybe_account_state?.get_extension::() { - if memo_transfer.require_incoming_transfer_memos.into() && memo.is_none() { - return Err( - "Error: Recipient expects a transfer memo, but none was provided. \ - Provide a memo using `--with-memo`." - .into(), - ); - } - } - - true - } else if recipient_account_owner == system_program::id() { - false - } else if recipient_account_owner == config.program_id { - return Err( - "Error: Recipient is owned by this token program, but is not a token account." - .into(), - ); - } else if VALID_TOKEN_PROGRAM_IDS.contains(&recipient_account_owner) { - return Err(format!( - "Error: Recipient is owned by {}, but the token mint is owned by {}.", - recipient_account_owner, config.program_id - ) - .into()); - } else if allow_non_system_account_recipient { - false - } else { - return Err("Error: The recipient address is not owned by the System Program. \ - Add `--allow-non-system-account-recipient` to complete the transfer.".into()); - } - } - // if it doesnt exist, it definitely isnt a token account! - // we gate transfer with a different flag - else if maybe_recipient_account_data.is_none() && allow_unfunded_recipient { - false - } else { - return Err("Error: The recipient address is not funded. \ - Add `--allow-unfunded-recipient` to complete the transfer." - .into()); - } - } else { - // in offline mode we gotta trust them - no_recipient_is_ata_owner - }; - - // now if its a token account, life is ez - let (recipient_token_account, fundable_owner) = if recipient_is_token_account { - (recipient, None) - } - // but if not, we need to determine if we can or should create an ata for recipient - else { - // first, get the ata address - let recipient_token_account = token.get_associated_token_address(&recipient); - - println_display( - config, - format!( - " Recipient associated token account: {}", - recipient_token_account - ), - ); - - // if we can fetch it to determine if it exists, do so - let needs_funding = if !config.sign_only { - if let Some(recipient_token_account_data) = config - .program_client - .get_account(recipient_token_account) - .await? - { - let recipient_token_account_owner = recipient_token_account_data.owner; - - if let Ok(account_state) = - StateWithExtensionsOwned::::unpack(recipient_token_account_data.data) - { - if let Ok(memo_transfer) = account_state.get_extension::() { - if memo_transfer.require_incoming_transfer_memos.into() && memo.is_none() { - return Err( - "Error: Recipient expects a transfer memo, but none was provided. \ - Provide a memo using `--with-memo`." - .into(), - ); - } - } - } - - if recipient_token_account_owner == system_program::id() { - true - } else if recipient_token_account_owner == config.program_id { - false - } else { - return Err( - format!("Error: Unsupported recipient address: {}", recipient).into(), - ); - } - } else { - true - } - } - // otherwise trust the cli flag - else { - fund_recipient - }; - - // and now we determine if we will actually fund it, based on its need and our - // willingness - let fundable_owner = if needs_funding { - if confidential_transfer_args.is_some() { - return Err( - "Error: Recipient's associated token account does not exist. \ - Accounts cannot be funded for confidential transfers." - .into(), - ); - } else if fund_recipient { - println_display( - config, - format!(" Funding recipient: {}", recipient_token_account,), - ); - - Some(recipient) - } else { - return Err( - "Error: Recipient's associated token account does not exist. \ - Add `--fund-recipient` to fund their account" - .into(), - ); - } - } else { - None - }; - - (recipient_token_account, fundable_owner) - }; - - // set up memo if provided... - if let Some(text) = memo { - token.with_memo(text, vec![config.default_signer()?.pubkey()]); - } - - // fetch confidential transfer info for recipient and auditor - let (recipient_elgamal_pubkey, auditor_elgamal_pubkey) = if let Some(args) = - confidential_transfer_args - { - if !config.sign_only { - // we can use the mint data from the start of the function, but will require - // non-trivial amount of refactoring the code due to ownership; for now, we - // fetch the mint a second time. This can potentially be optimized - // in the future. - let confidential_transfer_mint = config.get_account_checked(&token_pubkey).await?; - let mint_state = - StateWithExtensionsOwned::::unpack(confidential_transfer_mint.data) - .map_err(|_| format!("Could not deserialize token mint {}", token_pubkey))?; - - let auditor_elgamal_pubkey = if let Ok(confidential_transfer_mint) = - mint_state.get_extension::() - { - let expected_auditor_elgamal_pubkey = Option::::from( - confidential_transfer_mint.auditor_elgamal_pubkey, - ); - - // if auditor ElGamal pubkey is provided, check consistency with the one in the - // mint if auditor ElGamal pubkey is not provided, then use the - // expected one from the mint, which could also be `None` if - // auditing is disabled - if args.auditor_elgamal_pubkey.is_some() - && expected_auditor_elgamal_pubkey != args.auditor_elgamal_pubkey - { - return Err(format!( - "Mint {} has confidential transfer auditor {}, but {} was provided", - token_pubkey, - expected_auditor_elgamal_pubkey - .map(|pubkey| pubkey.to_string()) - .unwrap_or_else(|| "disabled".to_string()), - args.auditor_elgamal_pubkey.unwrap(), - ) - .into()); - } - - expected_auditor_elgamal_pubkey - } else { - return Err(format!( - "Mint {} does not support confidential transfers", - token_pubkey - ) - .into()); - }; - - let recipient_account = config.get_account_checked(&recipient_token_account).await?; - let recipient_elgamal_pubkey = - StateWithExtensionsOwned::::unpack(recipient_account.data)? - .get_extension::()? - .elgamal_pubkey; - - (Some(recipient_elgamal_pubkey), auditor_elgamal_pubkey) - } else { - let recipient_elgamal_pubkey = args - .recipient_elgamal_pubkey - .expect("Recipient ElGamal pubkey must be provided"); - let auditor_elgamal_pubkey = args - .auditor_elgamal_pubkey - .expect("Auditor ElGamal pubkey must be provided"); - - (Some(recipient_elgamal_pubkey), Some(auditor_elgamal_pubkey)) - } - } else { - (None, None) - }; - - // ...and, finally, the transfer - let res = match (fundable_owner, maybe_fee, confidential_transfer_args) { - (Some(recipient_owner), None, None) => { - token - .create_recipient_associated_account_and_transfer( - &sender, - &recipient_token_account, - &recipient_owner, - &sender_owner, - transfer_balance, - maybe_fee, - &bulk_signers, - ) - .await? - } - (Some(_), _, _) => { - panic!("Recipient account cannot be created for transfer with fees or confidential transfers"); - } - (None, Some(fee), None) => { - token - .transfer_with_fee( - &sender, - &recipient_token_account, - &sender_owner, - transfer_balance, - fee, - &bulk_signers, - ) - .await? - } - (None, None, Some(args)) => { - // deserialize `pod` ElGamal pubkeys - let recipient_elgamal_pubkey: elgamal::ElGamalPubkey = recipient_elgamal_pubkey - .unwrap() - .try_into() - .expect("Invalid recipient ElGamal pubkey"); - let auditor_elgamal_pubkey = auditor_elgamal_pubkey.map(|pubkey| { - let auditor_elgamal_pubkey: elgamal::ElGamalPubkey = - pubkey.try_into().expect("Invalid auditor ElGamal pubkey"); - auditor_elgamal_pubkey - }); - - let context_state_authority = config.fee_payer()?; - let equality_proof_context_state_account = Keypair::new(); - let equality_proof_pubkey = equality_proof_context_state_account.pubkey(); - let ciphertext_validity_proof_context_state_account = Keypair::new(); - let ciphertext_validity_proof_pubkey = - ciphertext_validity_proof_context_state_account.pubkey(); - let range_proof_context_state_account = Keypair::new(); - let range_proof_pubkey = range_proof_context_state_account.pubkey(); - - let transfer_context_state_accounts = TransferSplitContextStateAccounts { - equality_proof: &equality_proof_pubkey, - ciphertext_validity_proof: &ciphertext_validity_proof_pubkey, - range_proof: &range_proof_pubkey, - authority: &context_state_authority.pubkey(), - no_op_on_uninitialized_split_context_state: false, - close_split_context_state_accounts: None, - }; - - let state = token.get_account_info(&sender).await.unwrap(); - let extension = state - .get_extension::() - .unwrap(); - let transfer_account_info = TransferAccountInfo::new(extension); - - let ( - equality_proof_data, - ciphertext_validity_proof_data, - range_proof_data, - source_decrypt_handles, - ) = transfer_account_info - .generate_split_transfer_proof_data( - transfer_balance, - &args.sender_elgamal_keypair, - &args.sender_aes_key, - &recipient_elgamal_pubkey, - auditor_elgamal_pubkey.as_ref(), - ) - .unwrap(); - - // setup proofs - let _ = try_join!( - token.create_range_proof_context_state_for_transfer( - transfer_context_state_accounts, - &range_proof_data, - &range_proof_context_state_account, - ), - token.create_equality_proof_context_state_for_transfer( - transfer_context_state_accounts, - &equality_proof_data, - &equality_proof_context_state_account, - ), - token.create_ciphertext_validity_proof_context_state_for_transfer( - transfer_context_state_accounts, - &ciphertext_validity_proof_data, - &ciphertext_validity_proof_context_state_account, - ) - )?; - - // do the transfer - let transfer_result = token - .confidential_transfer_transfer_with_split_proofs( - &sender, - &recipient_token_account, - &sender_owner, - transfer_context_state_accounts, - transfer_balance, - Some(transfer_account_info), - &args.sender_aes_key, - &source_decrypt_handles, - &bulk_signers, - ) - .await?; - - // close context state accounts - let context_state_authority_pubkey = context_state_authority.pubkey(); - let close_context_state_signers = &[context_state_authority]; - let _ = try_join!( - token.confidential_transfer_close_context_state( - &equality_proof_pubkey, - &sender, - &context_state_authority_pubkey, - close_context_state_signers, - ), - token.confidential_transfer_close_context_state( - &ciphertext_validity_proof_pubkey, - &sender, - &context_state_authority_pubkey, - close_context_state_signers, - ), - token.confidential_transfer_close_context_state( - &range_proof_pubkey, - &sender, - &context_state_authority_pubkey, - close_context_state_signers, - ), - )?; - - transfer_result - } - (None, Some(_), Some(_)) => { - panic!("Confidential transfer with fee is not yet supported."); - } - (None, None, None) => { - token - .transfer( - &sender, - &recipient_token_account, - &sender_owner, - transfer_balance, - &bulk_signers, - ) - .await? - } - }; - - let tx_return = finish_tx(config, &res, no_wait).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) - } - }) -} - -#[allow(clippy::too_many_arguments)] -async fn command_burn( - config: &Config<'_>, - account: Pubkey, - owner: Pubkey, - ui_amount: f64, - mint_address: Option, - mint_decimals: Option, - use_unchecked_instruction: bool, - memo: Option, - bulk_signers: BulkSigners, -) -> CommandResult { - println_display( - config, - format!("Burn {} tokens\n Source: {}", ui_amount, account), - ); - - let mint_address = config.check_account(&account, mint_address).await?; - let mint_info = config.get_mint_info(&mint_address, mint_decimals).await?; - let amount = spl_token::ui_amount_to_amount(ui_amount, mint_info.decimals); - let decimals = if use_unchecked_instruction { - None - } else { - Some(mint_info.decimals) - }; - - let token = token_client_from_config(config, &mint_info.address, decimals)?; - if let Some(text) = memo { - token.with_memo(text, vec![config.default_signer()?.pubkey()]); - } - - let res = token.burn(&account, &owner, amount, &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) - } - }) -} - -#[allow(clippy::too_many_arguments)] -async fn command_mint( - config: &Config<'_>, - token: Pubkey, - ui_amount: f64, - recipient: Pubkey, - mint_info: MintInfo, - mint_authority: Pubkey, - use_unchecked_instruction: bool, - memo: Option, - bulk_signers: BulkSigners, -) -> CommandResult { - println_display( - config, - format!( - "Minting {} tokens\n Token: {}\n Recipient: {}", - ui_amount, token, recipient - ), - ); - - let amount = spl_token::ui_amount_to_amount(ui_amount, mint_info.decimals); - let decimals = if use_unchecked_instruction { - None - } else { - Some(mint_info.decimals) - }; - - let token = token_client_from_config(config, &mint_info.address, decimals)?; - if let Some(text) = memo { - token.with_memo(text, vec![config.default_signer()?.pubkey()]); - } - - let res = token - .mint_to(&recipient, &mint_authority, amount, &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_freeze( - config: &Config<'_>, - account: Pubkey, - mint_address: Option, - freeze_authority: Pubkey, - bulk_signers: BulkSigners, -) -> CommandResult { - let mint_address = config.check_account(&account, mint_address).await?; - let mint_info = config.get_mint_info(&mint_address, None).await?; - - println_display( - config, - format!( - "Freezing account: {}\n Token: {}", - account, mint_info.address - ), - ); - - // we dont use the decimals from mint_info because its not need and in sign-only - // its wrong - let token = token_client_from_config(config, &mint_info.address, None)?; - let res = token - .freeze(&account, &freeze_authority, &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_thaw( - config: &Config<'_>, - account: Pubkey, - mint_address: Option, - freeze_authority: Pubkey, - bulk_signers: BulkSigners, -) -> CommandResult { - let mint_address = config.check_account(&account, mint_address).await?; - let mint_info = config.get_mint_info(&mint_address, None).await?; - - println_display( - config, - format!( - "Thawing account: {}\n Token: {}", - account, mint_info.address - ), - ); - - // we dont use the decimals from mint_info because its not need and in sign-only - // its wrong - let token = token_client_from_config(config, &mint_info.address, None)?; - let res = token - .thaw(&account, &freeze_authority, &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_wrap( - config: &Config<'_>, - sol: f64, - wallet_address: Pubkey, - wrapped_sol_account: Option, - immutable_owner: bool, - bulk_signers: BulkSigners, -) -> CommandResult { - let lamports = sol_to_lamports(sol); - let token = native_token_client_from_config(config)?; - - let account = - wrapped_sol_account.unwrap_or_else(|| token.get_associated_token_address(&wallet_address)); - - println_display(config, format!("Wrapping {} SOL into {}", sol, account)); - - if !config.sign_only { - if let Some(account_data) = config.program_client.get_account(account).await? { - if account_data.owner != system_program::id() { - return Err(format!("Error: Account already exists: {}", account).into()); - } - } - - check_wallet_balance(config, &wallet_address, lamports).await?; - } - - let res = if immutable_owner { - if config.program_id == spl_token::id() { - return Err(format!( - "Specified --immutable, but token program {} does not support the extension", - config.program_id - ) - .into()); - } - - token - .wrap(&account, &wallet_address, lamports, &bulk_signers) - .await? - } else { - // this case is hit for a token22 ata, which is always immutable. but it does - // the right thing anyway - token - .wrap_with_mutable_ownership(&account, &wallet_address, lamports, &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_unwrap( - config: &Config<'_>, - wallet_address: Pubkey, - maybe_account: Option, - bulk_signers: BulkSigners, -) -> CommandResult { - let use_associated_account = maybe_account.is_none(); - let token = native_token_client_from_config(config)?; - - let account = - maybe_account.unwrap_or_else(|| token.get_associated_token_address(&wallet_address)); - - println_display(config, format!("Unwrapping {}", account)); - - if !config.sign_only { - let account_data = config.get_account_checked(&account).await?; - - if !use_associated_account { - let account_state = StateWithExtensionsOwned::::unpack(account_data.data)?; - - if account_state.base.mint != *token.get_address() { - return Err(format!("{} is not a native token account", account).into()); - } - } - - if account_data.lamports == 0 { - if use_associated_account { - return Err("No wrapped SOL in associated account; did you mean to specify an auxiliary address?".to_string().into()); - } else { - return Err(format!("No wrapped SOL in {}", account).into()); - } - } - - println_display( - config, - format!(" Amount: {} SOL", lamports_to_sol(account_data.lamports)), - ); - } - - println_display(config, format!(" Recipient: {}", &wallet_address)); - - let res = token - .close_account(&account, &wallet_address, &wallet_address, &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) - } - }) -} - -#[allow(clippy::too_many_arguments)] -async fn command_approve( - config: &Config<'_>, - account: Pubkey, - owner: Pubkey, - ui_amount: f64, - delegate: Pubkey, - mint_address: Option, - mint_decimals: Option, - use_unchecked_instruction: bool, - bulk_signers: BulkSigners, -) -> CommandResult { - println_display( - config, - format!( - "Approve {} tokens\n Account: {}\n Delegate: {}", - ui_amount, account, delegate - ), - ); - - let mint_address = config.check_account(&account, mint_address).await?; - let mint_info = config.get_mint_info(&mint_address, mint_decimals).await?; - let amount = spl_token::ui_amount_to_amount(ui_amount, mint_info.decimals); - let decimals = if use_unchecked_instruction { - None - } else { - Some(mint_info.decimals) - }; - - let token = token_client_from_config(config, &mint_info.address, decimals)?; - let res = token - .approve(&account, &delegate, &owner, amount, &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_revoke( - config: &Config<'_>, - account: Pubkey, - owner: Pubkey, - delegate: Option, - bulk_signers: BulkSigners, -) -> CommandResult { - let (mint_pubkey, delegate) = if !config.sign_only { - let source_account = config.get_account_checked(&account).await?; - let source_state = StateWithExtensionsOwned::::unpack(source_account.data) - .map_err(|_| format!("Could not deserialize token account {}", account))?; - - let delegate = if let COption::Some(delegate) = source_state.base.delegate { - Some(delegate) - } else { - None - }; - - (source_state.base.mint, delegate) - } else { - // default is safe here because revoke doesnt use it - (Pubkey::default(), delegate) - }; - - if let Some(delegate) = delegate { - println_display( - config, - format!( - "Revoking approval\n Account: {}\n Delegate: {}", - account, delegate - ), - ); - } else { - return Err(format!("No delegate on account {}", account).into()); - } - - let token = token_client_from_config(config, &mint_pubkey, None)?; - let res = token.revoke(&account, &owner, &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_close( - config: &Config<'_>, - account: Pubkey, - close_authority: Pubkey, - recipient: Pubkey, - bulk_signers: BulkSigners, -) -> CommandResult { - let mut results = vec![]; - let token = if !config.sign_only { - let source_account = config.get_account_checked(&account).await?; - - let source_state = StateWithExtensionsOwned::::unpack(source_account.data) - .map_err(|_| format!("Could not deserialize token account {}", account))?; - let source_amount = source_state.base.amount; - - if !source_state.base.is_native() && source_amount > 0 { - return Err(format!( - "Account {} still has {} tokens; empty the account in order to close it.", - account, source_amount, - ) - .into()); - } - - let token = token_client_from_config(config, &source_state.base.mint, None)?; - if let Ok(extension) = source_state.get_extension::() { - if u64::from(extension.withheld_amount) != 0 { - let res = token.harvest_withheld_tokens_to_mint(&[&account]).await?; - let tx_return = finish_tx(config, &res, false).await?; - results.push(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) - } - }); - } - } - - token - } else { - // default is safe here because close doesnt use it - token_client_from_config(config, &Pubkey::default(), None)? - }; - - let res = token - .close_account(&account, &recipient, &close_authority, &bulk_signers) - .await?; - - let tx_return = finish_tx(config, &res, false).await?; - results.push(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) - } - }); - Ok(results.join("")) -} - -async fn command_close_mint( - config: &Config<'_>, - token_pubkey: Pubkey, - close_authority: Pubkey, - recipient: Pubkey, - bulk_signers: BulkSigners, -) -> CommandResult { - if !config.sign_only { - let mint_account = config.get_account_checked(&token_pubkey).await?; - - let mint_state = StateWithExtensionsOwned::::unpack(mint_account.data) - .map_err(|_| format!("Could not deserialize token mint {}", token_pubkey))?; - let mint_supply = mint_state.base.supply; - - if mint_supply > 0 { - return Err(format!( - "Mint {} still has {} outstanding tokens; these must be burned before closing the mint.", - token_pubkey, mint_supply, - ) - .into()); - } - - if let Ok(mint_close_authority) = mint_state.get_extension::() { - let mint_close_authority_pubkey = - Option::::from(mint_close_authority.close_authority); - - if mint_close_authority_pubkey != Some(close_authority) { - return Err(format!( - "Mint {} has close authority {}, but {} was provided", - token_pubkey, - mint_close_authority_pubkey - .map(|pubkey| pubkey.to_string()) - .unwrap_or_else(|| "disabled".to_string()), - close_authority - ) - .into()); - } - } else { - return Err(format!("Mint {} does not support close authority", token_pubkey).into()); - } - } - - let token = token_client_from_config(config, &token_pubkey, None)?; - let res = token - .close_account(&token_pubkey, &recipient, &close_authority, &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_balance(config: &Config<'_>, address: Pubkey) -> CommandResult { - let balance = config - .rpc_client - .get_token_account_balance(&address) - .await - .map_err(|_| format!("Could not find token account {}", address))?; - let cli_token_amount = CliTokenAmount { amount: balance }; - Ok(config.output_format.formatted_string(&cli_token_amount)) -} - -async fn command_supply(config: &Config<'_>, token: Pubkey) -> CommandResult { - let supply = config.rpc_client.get_token_supply(&token).await?; - let cli_token_amount = CliTokenAmount { amount: supply }; - Ok(config.output_format.formatted_string(&cli_token_amount)) -} - -async fn command_accounts( - config: &Config<'_>, - maybe_token: Option, - owner: Pubkey, - account_filter: AccountFilter, - print_addresses_only: bool, -) -> CommandResult { - let filters = if let Some(token_pubkey) = maybe_token { - let _ = config.get_mint_info(&token_pubkey, None).await?; - vec![TokenAccountsFilter::Mint(token_pubkey)] - } else if config.restrict_to_program_id { - vec![TokenAccountsFilter::ProgramId(config.program_id)] - } else { - vec![ - TokenAccountsFilter::ProgramId(spl_token::id()), - TokenAccountsFilter::ProgramId(spl_token_2022::id()), - ] - }; - - let mut accounts = vec![]; - for filter in filters { - accounts.push( - config - .rpc_client - .get_token_accounts_by_owner(&owner, filter) - .await?, - ); - } - let accounts = accounts.into_iter().flatten().collect(); - - let cli_token_accounts = - sort_and_parse_token_accounts(&owner, accounts, maybe_token.is_some(), account_filter)?; - - if print_addresses_only { - Ok(cli_token_accounts - .accounts - .into_iter() - .flatten() - .map(|a| a.address) - .collect::>() - .join("\n")) - } else { - Ok(config.output_format.formatted_string(&cli_token_accounts)) - } -} - -async fn command_address( - config: &Config<'_>, - token: Option, - owner: Pubkey, -) -> CommandResult { - let mut cli_address = CliWalletAddress { - wallet_address: owner.to_string(), - ..CliWalletAddress::default() - }; - if let Some(token) = token { - config.get_mint_info(&token, None).await?; - let associated_token_address = - get_associated_token_address_with_program_id(&owner, &token, &config.program_id); - cli_address.associated_token_address = Some(associated_token_address.to_string()); - } - Ok(config.output_format.formatted_string(&cli_address)) -} - -async fn command_display(config: &Config<'_>, address: Pubkey) -> CommandResult { - let account_data = config.get_account_checked(&address).await?; - - let (decimals, has_permanent_delegate) = - if let Some(mint_address) = get_token_account_mint(&account_data.data) { - let mint_account = config.get_account_checked(&mint_address).await?; - let mint_state = StateWithExtensionsOwned::::unpack(mint_account.data) - .map_err(|_| format!("Could not deserialize token mint {}", mint_address))?; - - let has_permanent_delegate = - if let Ok(permanent_delegate) = mint_state.get_extension::() { - Option::::from(permanent_delegate.delegate).is_some() - } else { - false - }; - - (Some(mint_state.base.decimals), has_permanent_delegate) - } else { - (None, false) - }; - - let token_data = parse_token(&account_data.data, decimals); - - match token_data { - Ok(TokenAccountType::Account(account)) => { - let mint_address = Pubkey::from_str(&account.mint)?; - let owner = Pubkey::from_str(&account.owner)?; - let associated_address = get_associated_token_address_with_program_id( - &owner, - &mint_address, - &config.program_id, - ); - - let cli_output = CliTokenAccount { - address: address.to_string(), - program_id: config.program_id.to_string(), - is_associated: associated_address == address, - account, - has_permanent_delegate, - }; - - Ok(config.output_format.formatted_string(&cli_output)) - } - Ok(TokenAccountType::Mint(mint)) => { - let epoch_info = config.rpc_client.get_epoch_info().await?; - let cli_output = CliMint { - address: address.to_string(), - epoch: epoch_info.epoch, - program_id: config.program_id.to_string(), - mint, - }; - - Ok(config.output_format.formatted_string(&cli_output)) - } - Ok(TokenAccountType::Multisig(multisig)) => { - let cli_output = CliMultisig { - address: address.to_string(), - program_id: config.program_id.to_string(), - multisig, - }; - - Ok(config.output_format.formatted_string(&cli_output)) - } - Err(e) => Err(e.into()), - } -} - -async fn command_gc( - config: &Config<'_>, - owner: Pubkey, - close_empty_associated_accounts: bool, - bulk_signers: BulkSigners, -) -> CommandResult { - println_display( - config, - format!( - "Fetching token accounts associated with program {}", - config.program_id - ), - ); - let accounts = config - .rpc_client - .get_token_accounts_by_owner(&owner, TokenAccountsFilter::ProgramId(config.program_id)) - .await?; - if accounts.is_empty() { - println_display(config, "Nothing to do".to_string()); - return Ok("".to_string()); - } - - let mut accounts_by_token = HashMap::new(); - - for keyed_account in accounts { - if let UiAccountData::Json(parsed_account) = keyed_account.account.data { - if let Ok(TokenAccountType::Account(ui_token_account)) = - serde_json::from_value(parsed_account.parsed) - { - let frozen = ui_token_account.state == UiAccountState::Frozen; - let decimals = ui_token_account.token_amount.decimals; - - let token = ui_token_account - .mint - .parse::() - .unwrap_or_else(|err| panic!("Invalid mint: {}", err)); - let token_account = keyed_account - .pubkey - .parse::() - .unwrap_or_else(|err| panic!("Invalid token account: {}", err)); - let token_amount = ui_token_account - .token_amount - .amount - .parse::() - .unwrap_or_else(|err| panic!("Invalid token amount: {}", err)); - - let close_authority = ui_token_account.close_authority.map_or(owner, |s| { - s.parse::() - .unwrap_or_else(|err| panic!("Invalid close authority: {}", err)) - }); - - let entry = accounts_by_token - .entry((token, decimals)) - .or_insert_with(HashMap::new); - entry.insert(token_account, (token_amount, frozen, close_authority)); - } - } - } - - let mut results = vec![]; - for ((token_pubkey, decimals), accounts) in accounts_by_token.into_iter() { - println_display(config, format!("Processing token: {}", token_pubkey)); - - let token = token_client_from_config(config, &token_pubkey, Some(decimals))?; - let total_balance: u64 = accounts.values().map(|account| account.0).sum(); - - let associated_token_account = token.get_associated_token_address(&owner); - if !accounts.contains_key(&associated_token_account) && total_balance > 0 { - token.create_associated_token_account(&owner).await?; - } - - for (address, (amount, frozen, close_authority)) in accounts { - let is_associated = address == associated_token_account; - - // only close the associated account if --close-empty-associated-accounts is - // provided - if is_associated && !close_empty_associated_accounts { - continue; - } - - // never close the associated account if *any* account carries a balance - if is_associated && total_balance > 0 { - continue; - } - - // dont attempt to close frozen accounts - if frozen { - continue; - } - - if is_associated { - println!("Closing associated account {}", address); - } - - // this logic is quite fiendish, but its more readable this way than if/else - let maybe_res = match (close_authority == owner, is_associated, amount == 0) { - // owner authority, associated or auxiliary, empty -> close - (true, _, true) => Some( - token - .close_account(&address, &owner, &owner, &bulk_signers) - .await, - ), - // owner authority, auxiliary, nonempty -> empty and close - (true, false, false) => Some( - token - .empty_and_close_account( - &address, - &owner, - &associated_token_account, - &owner, - &bulk_signers, - ) - .await, - ), - // separate authority, auxiliary, nonempty -> transfer - (false, false, false) => Some( - token - .transfer( - &address, - &associated_token_account, - &owner, - amount, - &bulk_signers, - ) - .await, - ), - // separate authority, associated or auxiliary, empty -> print warning - (false, _, true) => { - println_display( - config, - format!( - "Note: skipping {} due to separate close authority {}; \ - revoke authority and rerun gc, or rerun gc with --owner", - address, close_authority - ), - ); - None - } - // anything else, including a nonempty associated account -> unreachable - (_, _, _) => unreachable!(), - }; - - if let Some(res) = maybe_res { - let tx_return = finish_tx(config, &res?, false).await?; - - results.push(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) - } - }); - }; - } - } - - Ok(results.join("")) -} - -async fn command_sync_native(config: &Config<'_>, native_account_address: Pubkey) -> CommandResult { - let token = native_token_client_from_config(config)?; - - if !config.sign_only { - let account_data = config.get_account_checked(&native_account_address).await?; - let account_state = StateWithExtensionsOwned::::unpack(account_data.data)?; - - if account_state.base.mint != *token.get_address() { - return Err(format!("{} is not a native token account", native_account_address).into()); - } - } - - let res = token.sync_native(&native_account_address).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_withdraw_excess_lamports( - config: &Config<'_>, - source_account: Pubkey, - destination_account: Pubkey, - authority: Pubkey, - bulk_signers: Vec>, -) -> CommandResult { - // default is safe here because withdraw_excess_lamports doesn't use it - let token = token_client_from_config(config, &Pubkey::default(), None)?; - println_display( - config, - format!( - "Withdrawing excess lamports\n Sender: {}\n Destination: {}", - source_account, destination_account - ), - ); - - let res = token - .withdraw_excess_lamports( - &source_account, - &destination_account, - &authority, - &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) - } - }) -} - -// both enables and disables required transfer memos, via enable_memos bool -async fn command_required_transfer_memos( - config: &Config<'_>, - token_account_address: Pubkey, - owner: Pubkey, - bulk_signers: BulkSigners, - enable_memos: bool, -) -> CommandResult { - if config.sign_only { - panic!("Config can not be sign-only for enabling/disabling required transfer memos."); - } - - let account = config.get_account_checked(&token_account_address).await?; - let current_account_len = account.data.len(); - - let state_with_extension = StateWithExtensionsOwned::::unpack(account.data)?; - let token = token_client_from_config(config, &state_with_extension.base.mint, None)?; - - // Reallocation (if needed) - let mut existing_extensions: Vec = state_with_extension.get_extension_types()?; - if existing_extensions.contains(&ExtensionType::MemoTransfer) { - let extension_state = state_with_extension - .get_extension::()? - .require_incoming_transfer_memos - .into(); - - if extension_state == enable_memos { - return Ok(format!( - "Required transfer memos were already {}", - if extension_state { - "enabled" - } else { - "disabled" - } - )); - } - } else { - existing_extensions.push(ExtensionType::MemoTransfer); - let needed_account_len = - ExtensionType::try_calculate_account_len::(&existing_extensions)?; - if needed_account_len > current_account_len { - token - .reallocate( - &token_account_address, - &owner, - &[ExtensionType::MemoTransfer], - &bulk_signers, - ) - .await?; - } - } - - let res = if enable_memos { - token - .enable_required_transfer_memos(&token_account_address, &owner, &bulk_signers) - .await - } else { - token - .disable_required_transfer_memos(&token_account_address, &owner, &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) - } - }) -} - -// both enables and disables cpi guard, via enable_guard bool -async fn command_cpi_guard( - config: &Config<'_>, - token_account_address: Pubkey, - owner: Pubkey, - bulk_signers: BulkSigners, - enable_guard: bool, -) -> CommandResult { - if config.sign_only { - panic!("Config can not be sign-only for enabling/disabling required transfer memos."); - } - - let account = config.get_account_checked(&token_account_address).await?; - let current_account_len = account.data.len(); - - let state_with_extension = StateWithExtensionsOwned::::unpack(account.data)?; - let token = token_client_from_config(config, &state_with_extension.base.mint, None)?; - - // reallocation (if needed) - let mut existing_extensions: Vec = state_with_extension.get_extension_types()?; - if existing_extensions.contains(&ExtensionType::CpiGuard) { - let extension_state = state_with_extension - .get_extension::()? - .lock_cpi - .into(); - - if extension_state == enable_guard { - return Ok(format!( - "CPI Guard was already {}", - if extension_state { - "enabled" - } else { - "disabled" - } - )); - } - } else { - existing_extensions.push(ExtensionType::CpiGuard); - let required_account_len = - ExtensionType::try_calculate_account_len::(&existing_extensions)?; - if required_account_len > current_account_len { - token - .reallocate( - &token_account_address, - &owner, - &[ExtensionType::CpiGuard], - &bulk_signers, - ) - .await?; - } - } - - let res = if enable_guard { - token - .enable_cpi_guard(&token_account_address, &owner, &bulk_signers) - .await - } else { - token - .disable_cpi_guard(&token_account_address, &owner, &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_update_metadata_pointer_address( - config: &Config<'_>, - token_pubkey: Pubkey, - authority: Pubkey, - new_metadata_address: Option, - bulk_signers: BulkSigners, -) -> CommandResult { - if config.sign_only { - panic!("Config can not be sign-only for updating metadata pointer address."); - } - - let token = token_client_from_config(config, &token_pubkey, None)?; - let res = token - .update_metadata_address(&authority, new_metadata_address, &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_update_default_account_state( - config: &Config<'_>, - token_pubkey: Pubkey, - freeze_authority: Pubkey, - new_default_state: AccountState, - bulk_signers: BulkSigners, -) -> CommandResult { - if !config.sign_only { - let mint_account = config.get_account_checked(&token_pubkey).await?; - - let mint_state = StateWithExtensionsOwned::::unpack(mint_account.data) - .map_err(|_| format!("Could not deserialize token mint {}", token_pubkey))?; - match mint_state.base.freeze_authority { - COption::None => { - return Err(format!("Mint {} has no freeze authority.", token_pubkey).into()) - } - COption::Some(mint_freeze_authority) => { - if mint_freeze_authority != freeze_authority { - return Err(format!( - "Mint {} has a freeze authority {}, {} provided", - token_pubkey, mint_freeze_authority, freeze_authority - ) - .into()); - } - } - } - - if let Ok(default_account_state) = mint_state.get_extension::() { - if default_account_state.state == u8::from(new_default_state) { - let state_string = match new_default_state { - AccountState::Frozen => "frozen", - AccountState::Initialized => "initialized", - _ => unreachable!(), - }; - return Err(format!( - "Mint {} already has default account state {}", - token_pubkey, state_string - ) - .into()); - } - } else { - return Err(format!( - "Mint {} does not support default account states", - token_pubkey - ) - .into()); - } - } - - let token = token_client_from_config(config, &token_pubkey, None)?; - let res = token - .set_default_account_state(&freeze_authority, &new_default_state, &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_withdraw_withheld_tokens( - config: &Config<'_>, - destination_token_account: Pubkey, - source_token_accounts: Vec, - authority: Pubkey, - include_mint: bool, - bulk_signers: BulkSigners, -) -> CommandResult { - if config.sign_only { - panic!("Config can not be sign-only for withdrawing withheld tokens."); - } - let destination_account = config - .get_account_checked(&destination_token_account) - .await?; - let destination_state = StateWithExtensionsOwned::::unpack(destination_account.data) - .map_err(|_| { - format!( - "Could not deserialize token account {}", - destination_token_account - ) - })?; - let token_pubkey = destination_state.base.mint; - destination_state - .get_extension::() - .map_err(|_| format!("Token mint {} has no transfer fee configured", token_pubkey))?; - - let token = token_client_from_config(config, &token_pubkey, None)?; - let mut results = vec![]; - if include_mint { - let res = token - .withdraw_withheld_tokens_from_mint( - &destination_token_account, - &authority, - &bulk_signers, - ) - .await; - let tx_return = finish_tx(config, &res?, false).await?; - results.push(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) - } - }); - } - - let source_refs = source_token_accounts.iter().collect::>(); - // this can be tweaked better, but keep it simple for now - const MAX_WITHDRAWAL_ACCOUNTS: usize = 25; - for sources in source_refs.chunks(MAX_WITHDRAWAL_ACCOUNTS) { - let res = token - .withdraw_withheld_tokens_from_accounts( - &destination_token_account, - &authority, - sources, - &bulk_signers, - ) - .await; - let tx_return = finish_tx(config, &res?, false).await?; - results.push(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) - } - }); - } - - Ok(results.join("")) -} - -async fn command_update_confidential_transfer_settings( - config: &Config<'_>, - token_pubkey: Pubkey, - authority: Pubkey, - auto_approve: Option, - auditor_pubkey: Option, - bulk_signers: Vec>, -) -> CommandResult { - let (new_auto_approve, new_auditor_pubkey) = if !config.sign_only { - let confidential_transfer_account = config.get_account_checked(&token_pubkey).await?; - - let mint_state = - StateWithExtensionsOwned::::unpack(confidential_transfer_account.data) - .map_err(|_| format!("Could not deserialize token mint {}", token_pubkey))?; - - if let Ok(confidential_transfer_mint) = - mint_state.get_extension::() - { - let expected_authority = Option::::from(confidential_transfer_mint.authority); - - if expected_authority != Some(authority) { - return Err(format!( - "Mint {} has confidential transfer authority {}, but {} was provided", - token_pubkey, - expected_authority - .map(|pubkey| pubkey.to_string()) - .unwrap_or_else(|| "disabled".to_string()), - authority - ) - .into()); - } - - let new_auto_approve = if let Some(auto_approve) = auto_approve { - auto_approve - } else { - bool::from(confidential_transfer_mint.auto_approve_new_accounts) - }; - - let new_auditor_pubkey = if let Some(auditor_pubkey) = auditor_pubkey { - auditor_pubkey.into() - } else { - Option::::from(confidential_transfer_mint.auditor_elgamal_pubkey) - }; - - (new_auto_approve, new_auditor_pubkey) - } else { - return Err(format!( - "Mint {} does not support confidential transfers", - token_pubkey - ) - .into()); - } - } else { - let new_auto_approve = auto_approve.expect("The approve policy must be provided"); - let new_auditor_pubkey = auditor_pubkey - .expect("The auditor encryption pubkey must be provided") - .into(); - - (new_auto_approve, new_auditor_pubkey) - }; - - println_display( - config, - format!( - "Updating confidential transfer settings for {}:", - token_pubkey, - ), - ); - - if auto_approve.is_some() { - println_display( - config, - format!( - " approve policy set to {}", - if new_auto_approve { "auto" } else { "manual" } - ), - ); - } - - if auditor_pubkey.is_some() { - if let Some(new_auditor_pubkey) = new_auditor_pubkey { - println_display( - config, - format!(" auditor encryption pubkey set to {}", new_auditor_pubkey,), - ); - } else { - println_display(config, " auditability disabled".to_string()) - } - } - - let token = token_client_from_config(config, &token_pubkey, None)?; - let res = token - .confidential_transfer_update_mint( - &authority, - new_auto_approve, - new_auditor_pubkey, - &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) - } - }) -} - -#[allow(clippy::too_many_arguments)] -async fn command_configure_confidential_transfer_account( - config: &Config<'_>, - maybe_token: Option, - owner: Pubkey, - maybe_account: Option, - maximum_credit_counter: Option, - elgamal_keypair: &ElGamalKeypair, - aes_key: &AeKey, - bulk_signers: BulkSigners, -) -> CommandResult { - if config.sign_only { - panic!("Sign-only is not yet supported."); - } - - let token_account_address = if let Some(account) = maybe_account { - account - } else { - let token_pubkey = - maybe_token.expect("Either a valid token or account address must be provided"); - let token = token_client_from_config(config, &token_pubkey, None)?; - token.get_associated_token_address(&owner) - }; - - let account = config.get_account_checked(&token_account_address).await?; - let current_account_len = account.data.len(); - - let state_with_extension = StateWithExtensionsOwned::::unpack(account.data)?; - let token = token_client_from_config(config, &state_with_extension.base.mint, None)?; - - // Reallocation (if needed) - let mut existing_extensions: Vec = state_with_extension.get_extension_types()?; - if !existing_extensions.contains(&ExtensionType::ConfidentialTransferAccount) { - existing_extensions.push(ExtensionType::ConfidentialTransferAccount); - let needed_account_len = - ExtensionType::try_calculate_account_len::(&existing_extensions)?; - if needed_account_len > current_account_len { - token - .reallocate( - &token_account_address, - &owner, - &[ExtensionType::ConfidentialTransferAccount], - &bulk_signers, - ) - .await?; - } - } - - let res = token - .confidential_transfer_configure_token_account( - &token_account_address, - &owner, - None, - maximum_credit_counter, - elgamal_keypair, - aes_key, - &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_enable_disable_confidential_transfers( - config: &Config<'_>, - maybe_token: Option, - owner: Pubkey, - maybe_account: Option, - bulk_signers: BulkSigners, - allow_confidential_credits: Option, - allow_non_confidential_credits: Option, -) -> CommandResult { - if config.sign_only { - panic!("Sign-only is not yet supported."); - } - - let token_account_address = if let Some(account) = maybe_account { - account - } else { - let token_pubkey = - maybe_token.expect("Either a valid token or account address must be provided"); - let token = token_client_from_config(config, &token_pubkey, None)?; - token.get_associated_token_address(&owner) - }; - - let account = config.get_account_checked(&token_account_address).await?; - - let state_with_extension = StateWithExtensionsOwned::::unpack(account.data)?; - let token = token_client_from_config(config, &state_with_extension.base.mint, None)?; - - let existing_extensions: Vec = state_with_extension.get_extension_types()?; - if !existing_extensions.contains(&ExtensionType::ConfidentialTransferAccount) { - panic!( - "Confidential transfer is not yet configured for this account. \ - Use `configure-confidential-transfer-account` command instead." - ); - } - - let res = if let Some(allow_confidential_credits) = allow_confidential_credits { - let extension_state = state_with_extension - .get_extension::()? - .allow_confidential_credits - .into(); - - if extension_state == allow_confidential_credits { - return Ok(format!( - "Confidential transfers are already {}", - if extension_state { - "enabled" - } else { - "disabled" - } - )); - } - - if allow_confidential_credits { - token - .confidential_transfer_enable_confidential_credits( - &token_account_address, - &owner, - &bulk_signers, - ) - .await - } else { - token - .confidential_transfer_disable_confidential_credits( - &token_account_address, - &owner, - &bulk_signers, - ) - .await - } - } else { - let allow_non_confidential_credits = - allow_non_confidential_credits.expect("Nothing to be done"); - let extension_state = state_with_extension - .get_extension::()? - .allow_non_confidential_credits - .into(); - - if extension_state == allow_non_confidential_credits { - return Ok(format!( - "Non-confidential transfers are already {}", - if extension_state { - "enabled" - } else { - "disabled" - } - )); - } - - if allow_non_confidential_credits { - token - .confidential_transfer_enable_non_confidential_credits( - &token_account_address, - &owner, - &bulk_signers, - ) - .await - } else { - token - .confidential_transfer_disable_non_confidential_credits( - &token_account_address, - &owner, - &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) - } - }) -} - -#[derive(PartialEq, Eq)] -enum ConfidentialInstructionType { - Deposit, - Withdraw, -} - -#[allow(clippy::too_many_arguments)] -async fn command_deposit_withdraw_confidential_tokens( - config: &Config<'_>, - token_pubkey: Pubkey, - owner: Pubkey, - maybe_account: Option, - bulk_signers: BulkSigners, - ui_amount: Option, - mint_decimals: Option, - instruction_type: ConfidentialInstructionType, - elgamal_keypair: Option<&ElGamalKeypair>, - aes_key: Option<&AeKey>, -) -> CommandResult { - if config.sign_only { - panic!("Sign-only is not yet supported."); - } - - // check if mint decimals provided is consistent - let mint_info = config.get_mint_info(&token_pubkey, mint_decimals).await?; - - if !config.sign_only && mint_decimals.is_some() && mint_decimals != Some(mint_info.decimals) { - return Err(format!( - "Decimals {} was provided, but actual value is {}", - mint_decimals.unwrap(), - mint_info.decimals - ) - .into()); - } - - let decimals = if let Some(decimals) = mint_decimals { - decimals - } else { - mint_info.decimals - }; - - // derive ATA if account address not provided - let token_account_address = if let Some(account) = maybe_account { - account - } else { - let token = token_client_from_config(config, &token_pubkey, Some(decimals))?; - token.get_associated_token_address(&owner) - }; - - let account = config.get_account_checked(&token_account_address).await?; - - let state_with_extension = StateWithExtensionsOwned::::unpack(account.data)?; - let token = token_client_from_config(config, &state_with_extension.base.mint, None)?; - - // the amount the user wants to deposit or withdraw, as an f64 - let maybe_amount = - ui_amount.map(|ui_amount| spl_token::ui_amount_to_amount(ui_amount, mint_info.decimals)); - - // the amount we will deposit or withdraw, as a u64 - let amount = if !config.sign_only && instruction_type == ConfidentialInstructionType::Deposit { - let current_balance = state_with_extension.base.amount; - let deposit_amount = maybe_amount.unwrap_or(current_balance); - - println_display( - config, - format!( - "Depositing {} confidential tokens", - spl_token::amount_to_ui_amount(deposit_amount, mint_info.decimals), - ), - ); - - if deposit_amount > current_balance { - return Err(format!( - "Error: Insufficient funds, current balance is {}", - spl_token_2022::amount_to_ui_amount_string_trimmed( - current_balance, - mint_info.decimals - ) - ) - .into()); - } - - deposit_amount - } else if !config.sign_only && instruction_type == ConfidentialInstructionType::Withdraw { - // // TODO: expose account balance decryption in token - // let aes_key = aes_key.expect("AES key must be provided"); - // let current_balance = token - // .confidential_transfer_get_available_balance_with_key( - // &token_account_address, - // aes_key, - // ) - // .await?; - let withdraw_amount = - maybe_amount.expect("ALL keyword is not currently supported for withdraw"); - - println_display( - config, - format!( - "Withdrawing {} confidential tokens", - spl_token::amount_to_ui_amount(withdraw_amount, mint_info.decimals) - ), - ); - - withdraw_amount - } else { - maybe_amount.unwrap() - }; - - let res = match instruction_type { - ConfidentialInstructionType::Deposit => { - token - .confidential_transfer_deposit( - &token_account_address, - &owner, - amount, - decimals, - &bulk_signers, - ) - .await? - } - ConfidentialInstructionType::Withdraw => { - let elgamal_keypair = elgamal_keypair.expect("ElGamal keypair must be provided"); - let aes_key = aes_key.expect("AES key must be provided"); - - let extension_state = - state_with_extension.get_extension::()?; - let withdraw_account_info = WithdrawAccountInfo::new(extension_state); - - let context_state_authority = config.fee_payer()?; - let context_state_keypair = Keypair::new(); - let context_state_pubkey = context_state_keypair.pubkey(); - - let withdraw_proof_data = - withdraw_account_info.generate_proof_data(amount, elgamal_keypair, aes_key)?; - - // setup proof - token - .create_withdraw_proof_context_state( - &context_state_pubkey, - &context_state_authority.pubkey(), - &withdraw_proof_data, - &context_state_keypair, - ) - .await?; - - // do the withdrawal - token - .confidential_transfer_withdraw( - &token_account_address, - &owner, - Some(&context_state_pubkey), - amount, - decimals, - Some(withdraw_account_info), - elgamal_keypair, - aes_key, - &bulk_signers, - ) - .await?; - - // close context state account - let context_state_authority_pubkey = context_state_authority.pubkey(); - let close_context_state_signers = &[context_state_authority]; - token - .confidential_transfer_close_context_state( - &context_state_pubkey, - &token_account_address, - &context_state_authority_pubkey, - close_context_state_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) - } - }) -} - -#[allow(clippy::too_many_arguments)] -async fn command_apply_pending_balance( - config: &Config<'_>, - maybe_token: Option, - owner: Pubkey, - maybe_account: Option, - bulk_signers: BulkSigners, - elgamal_keypair: &ElGamalKeypair, - aes_key: &AeKey, -) -> CommandResult { - if config.sign_only { - panic!("Sign-only is not yet supported."); - } - - // derive ATA if account address not provided - let token_account_address = if let Some(account) = maybe_account { - account - } else { - let token_pubkey = - maybe_token.expect("Either a valid token or account address must be provided"); - let token = token_client_from_config(config, &token_pubkey, None)?; - token.get_associated_token_address(&owner) - }; - - let account = config.get_account_checked(&token_account_address).await?; - - let state_with_extension = StateWithExtensionsOwned::::unpack(account.data)?; - let token = token_client_from_config(config, &state_with_extension.base.mint, None)?; - - let extension_state = state_with_extension.get_extension::()?; - let account_info = ApplyPendingBalanceAccountInfo::new(extension_state); - - let res = token - .confidential_transfer_apply_pending_balance( - &token_account_address, - &owner, - Some(account_info), - elgamal_keypair.secret(), - aes_key, - &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) - } - }) -} - -struct SignOnlyNeedsFullMintSpec {} -impl offline::ArgsConfig for SignOnlyNeedsFullMintSpec { - fn sign_only_arg<'a, 'b>(&self, arg: Arg<'a, 'b>) -> Arg<'a, 'b> { - arg.requires_all(&[MINT_ADDRESS_ARG.name, MINT_DECIMALS_ARG.name]) - } - fn signer_arg<'a, 'b>(&self, arg: Arg<'a, 'b>) -> Arg<'a, 'b> { - arg.requires_all(&[MINT_ADDRESS_ARG.name, MINT_DECIMALS_ARG.name]) - } -} - -struct SignOnlyNeedsMintDecimals {} -impl offline::ArgsConfig for SignOnlyNeedsMintDecimals { - fn sign_only_arg<'a, 'b>(&self, arg: Arg<'a, 'b>) -> Arg<'a, 'b> { - arg.requires_all(&[MINT_DECIMALS_ARG.name]) - } - fn signer_arg<'a, 'b>(&self, arg: Arg<'a, 'b>) -> Arg<'a, 'b> { - arg.requires_all(&[MINT_DECIMALS_ARG.name]) - } -} - -struct SignOnlyNeedsMintAddress {} -impl offline::ArgsConfig for SignOnlyNeedsMintAddress { - fn sign_only_arg<'a, 'b>(&self, arg: Arg<'a, 'b>) -> Arg<'a, 'b> { - arg.requires_all(&[MINT_ADDRESS_ARG.name]) - } - fn signer_arg<'a, 'b>(&self, arg: Arg<'a, 'b>) -> Arg<'a, 'b> { - arg.requires_all(&[MINT_ADDRESS_ARG.name]) - } -} - -struct SignOnlyNeedsDelegateAddress {} -impl offline::ArgsConfig for SignOnlyNeedsDelegateAddress { - fn sign_only_arg<'a, 'b>(&self, arg: Arg<'a, 'b>) -> Arg<'a, 'b> { - arg.requires_all(&[DELEGATE_ADDRESS_ARG.name]) - } - fn signer_arg<'a, 'b>(&self, arg: Arg<'a, 'b>) -> Arg<'a, 'b> { - arg.requires_all(&[DELEGATE_ADDRESS_ARG.name]) - } -} - -struct SignOnlyNeedsTransferLamports {} -impl offline::ArgsConfig for SignOnlyNeedsTransferLamports { - fn sign_only_arg<'a, 'b>(&self, arg: Arg<'a, 'b>) -> Arg<'a, 'b> { - arg.requires_all(&[TRANSFER_LAMPORTS_ARG.name]) - } - fn signer_arg<'a, 'b>(&self, arg: Arg<'a, 'b>) -> Arg<'a, 'b> { - arg.requires_all(&[TRANSFER_LAMPORTS_ARG.name]) - } -} - -struct ConfidentialTransferArgs { - sender_elgamal_keypair: ElGamalKeypair, - sender_aes_key: AeKey, - recipient_elgamal_pubkey: Option, - auditor_elgamal_pubkey: Option, -} - -fn minimum_signers_help_string() -> String { - format!( - "The minimum number of signers required to allow the operation. [{} <= M <= N]", - MIN_SIGNERS - ) -} - -fn multisig_member_help_string() -> String { - format!( - "The public keys for each of the N signing members of this account. [{} <= N <= {}]", - MIN_SIGNERS, MAX_SIGNERS - ) -} - -fn app<'a, 'b>( - default_decimals: &'a str, - minimum_signers_help: &'b str, - multisig_member_help: &'b str, -) -> App<'a, 'b> { - App::new(crate_name!()) - .about(crate_description!()) - .version(crate_version!()) - .setting(AppSettings::SubcommandRequiredElseHelp) - .arg( - Arg::with_name("config_file") - .short("C") - .long("config") - .value_name("PATH") - .takes_value(true) - .global(true) - .help("Configuration file to use"), - ) - .arg( - Arg::with_name("verbose") - .short("v") - .long("verbose") - .takes_value(false) - .global(true) - .help("Show additional information"), - ) - .arg( - Arg::with_name("output_format") - .long("output") - .value_name("FORMAT") - .global(true) - .takes_value(true) - .possible_values(&["json", "json-compact"]) - .help("Return information in specified output format"), - ) - .arg( - Arg::with_name("program_id") - .short("p") - .long("program-id") - .value_name("ADDRESS") - .takes_value(true) - .global(true) - .validator(is_valid_token_program_id) - .help("SPL Token program id"), - ) - .arg( - Arg::with_name("json_rpc_url") - .short("u") - .long("url") - .value_name("URL_OR_MONIKER") - .takes_value(true) - .global(true) - .validator(is_url_or_moniker) - .help( - "URL for Solana's JSON RPC or moniker (or their first letter): \ - [mainnet-beta, testnet, devnet, localhost] \ - Default from the configuration file." - ), - ) - .arg(fee_payer_arg().global(true)) - .arg( - Arg::with_name("use_unchecked_instruction") - .long("use-unchecked-instruction") - .takes_value(false) - .global(true) - .hidden(true) - .help("Use unchecked instruction if appropriate. Supports transfer, burn, mint, and approve."), - ) - .bench_subcommand() - .subcommand(SubCommand::with_name(CommandName::CreateToken.into()).about("Create a new token") - .arg( - Arg::with_name("token_keypair") - .value_name("TOKEN_KEYPAIR") - .validator(is_valid_signer) - .takes_value(true) - .index(1) - .help( - "Specify the token keypair. \ - This may be a keypair file or the ASK keyword. \ - [default: randomly generated keypair]" - ), - ) - .arg( - Arg::with_name("mint_authority") - .long("mint-authority") - .alias("owner") - .value_name("ADDRESS") - .validator(is_valid_pubkey) - .takes_value(true) - .help( - "Specify the mint authority address. \ - Defaults to the client keypair address." - ), - ) - .arg( - Arg::with_name("decimals") - .long("decimals") - .validator(is_mint_decimals) - .value_name("DECIMALS") - .takes_value(true) - .default_value(default_decimals) - .help("Number of base 10 digits to the right of the decimal place"), - ) - .arg( - Arg::with_name("enable_freeze") - .long("enable-freeze") - .takes_value(false) - .help( - "Enable the mint authority to freeze token accounts for this mint" - ), - ) - .arg( - Arg::with_name("enable_close") - .long("enable-close") - .takes_value(false) - .help( - "Enable the mint authority to close this mint" - ), - ) - .arg( - Arg::with_name("interest_rate") - .long("interest-rate") - .value_name("RATE_BPS") - .takes_value(true) - .help( - "Specify the interest rate in basis points. \ - Rate authority defaults to the mint authority." - ), - ) - .arg( - Arg::with_name("metadata_address") - .long("metadata-address") - .value_name("ADDRESS") - .takes_value(true) - .conflicts_with("enable_metadata") - .help( - "Specify address that stores token metadata." - ), - ) - .arg( - Arg::with_name("enable_non_transferable") - .long("enable-non-transferable") - .alias("enable-nontransferable") - .takes_value(false) - .help( - "Permanently force tokens to be non-transferable. Thay may still be burned." - ), - ) - .arg( - Arg::with_name("default_account_state") - .long("default-account-state") - .requires("enable_freeze") - .takes_value(true) - .possible_values(&["initialized", "frozen"]) - .help("Specify that accounts have a default state. \ - Note: specifying \"initialized\" adds an extension, which gives \ - the option of specifying default frozen accounts in the future. \ - This behavior is not the same as the default, which makes it \ - impossible to specify a default account state in the future."), - ) - .arg( - Arg::with_name("transfer_fee") - .long("transfer-fee") - .value_names(&["FEE_IN_BASIS_POINTS", "MAXIMUM_FEE"]) - .takes_value(true) - .number_of_values(2) - .help( - "Add a transfer fee to the mint. \ - The mint authority can set the fee and withdraw collected fees.", - ), - ) - .arg( - Arg::with_name("enable_permanent_delegate") - .long("enable-permanent-delegate") - .takes_value(false) - .help( - "Enable the mint authority to be permanent delegate for this mint" - ), - ) - .arg( - Arg::with_name("enable_confidential_transfers") - .long("enable-confidential-transfers") - .value_names(&["APPROVE-POLICY"]) - .takes_value(true) - .possible_values(&["auto", "manual"]) - .help( - "Enable accounts to make confidential transfers. If \"auto\" \ - is selected, then accounts are automatically approved to make \ - confidential transfers. If \"manual\" is selected, then the \ - confidential transfer mint authority must approve each account \ - before it can make confidential transfers." - ) - ) - .arg( - Arg::with_name("transfer_hook") - .long("transfer-hook") - .value_name("TRANSFER_HOOK_PROGRAM_ID") - .validator(is_valid_pubkey) - .takes_value(true) - .help("Enable the mint authority to set the transfer hook program for this mint"), - ) - .arg( - Arg::with_name("enable_metadata") - .long("enable-metadata") - .conflicts_with("metadata_address") - .takes_value(false) - .help("Enables metadata in the mint. The mint authority must initialize the metadata."), - ) - .nonce_args(true) - .arg(memo_arg()) - ) - .subcommand( - SubCommand::with_name(CommandName::SetInterestRate.into()) - .about("Set the interest rate for an interest-bearing token") - .arg( - Arg::with_name("token") - .validator(is_valid_pubkey) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .required(true) - .help("The interest-bearing token address"), - ) - .arg( - Arg::with_name("rate") - .value_name("RATE") - .takes_value(true) - .required(true) - .help("The new interest rate in basis points"), - ) - .arg( - Arg::with_name("rate_authority") - .long("rate-authority") - .validator(is_valid_signer) - .value_name("SIGNER") - .takes_value(true) - .help( - "Specify the rate authority keypair. \ - Defaults to the client keypair address." - ) - ) - ) - .subcommand( - SubCommand::with_name(CommandName::SetTransferHook.into()) - .about("Set the transfer hook program id for a token") - .arg( - Arg::with_name("token") - .validator(is_valid_pubkey) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .required(true) - .index(1) - .help("The token address with an existing transfer hook"), - ) - .arg( - Arg::with_name("new_program_id") - .validator(is_valid_pubkey) - .value_name("NEW_PROGRAM_ID") - .takes_value(true) - .required_unless("disable") - .index(2) - .help("The new transfer hook program id to set for this mint"), - ) - .arg( - Arg::with_name("disable") - .long("disable") - .takes_value(false) - .conflicts_with("new_program_id") - .help("Disable transfer hook functionality by setting the program id to None.") - ) - .arg( - Arg::with_name("authority") - .long("authority") - .alias("owner") - .validator(is_valid_signer) - .value_name("SIGNER") - .takes_value(true) - .help("Specify the authority keypair. Defaults to the client keypair address.") - ) - ) - .subcommand( - SubCommand::with_name(CommandName::InitializeMetadata.into()) - .about("Initialize metadata extension on a token mint") - .arg( - Arg::with_name("token") - .validator(is_valid_pubkey) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .required(true) - .index(1) - .help("The token address with no metadata present"), - ) - .arg( - Arg::with_name("name") - .value_name("TOKEN_NAME") - .takes_value(true) - .required(true) - .index(2) - .help("The name of the token to set in metadata"), - ) - .arg( - Arg::with_name("symbol") - .value_name("TOKEN_SYMBOL") - .takes_value(true) - .required(true) - .index(3) - .help("The symbol of the token to set in metadata"), - ) - .arg( - Arg::with_name("uri") - .value_name("TOKEN_URI") - .takes_value(true) - .required(true) - .index(4) - .help("The URI of the token to set in metadata"), - ) - .arg( - Arg::with_name("mint_authority") - .long("mint-authority") - .alias("owner") - .value_name("KEYPAIR") - .validator(is_valid_signer) - .takes_value(true) - .help( - "Specify the mint authority keypair. \ - This may be a keypair file or the ASK keyword. \ - Defaults to the client keypair." - ), - ) - .arg( - Arg::with_name("update_authority") - .long("update-authority") - .value_name("ADDRESS") - .validator(is_valid_pubkey) - .takes_value(true) - .help( - "Specify the update authority address. \ - Defaults to the client keypair address." - ), - ) - ) - .subcommand( - SubCommand::with_name(CommandName::UpdateMetadata.into()) - .about("Update metadata on a token mint that has the extension") - .arg( - Arg::with_name("token") - .validator(is_valid_pubkey) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .required(true) - .index(1) - .help("The token address with no metadata present"), - ) - .arg( - Arg::with_name("field") - .value_name("FIELD_NAME") - .takes_value(true) - .required(true) - .index(2) - .help("The name of the field to update. Can be a base field (\"name\", \"symbol\", or \"uri\") or any new field to add."), - ) - .arg( - Arg::with_name("value") - .value_name("VALUE_STRING") - .takes_value(true) - .index(3) - .required_unless("remove") - .help("The value for the field"), - ) - .arg( - Arg::with_name("remove") - .long("remove") - .takes_value(false) - .conflicts_with("value") - .help("Remove the key and value for the given field. Does not work with base fields: \"name\", \"symbol\", or \"uri\".") - ) - .arg( - Arg::with_name("authority") - .long("authority") - .validator(is_valid_signer) - .value_name("SIGNER") - .takes_value(true) - .help("Specify the metadata update authority keypair. Defaults to the client keypair.") - ) - .nonce_args(true) - .arg(transfer_lamports_arg()) - .offline_args_config(&SignOnlyNeedsTransferLamports{}), - ) - .subcommand( - SubCommand::with_name(CommandName::CreateAccount.into()) - .about("Create a new token account") - .arg( - Arg::with_name("token") - .validator(is_valid_pubkey) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The token that the account will hold"), - ) - .arg( - Arg::with_name("account_keypair") - .value_name("ACCOUNT_KEYPAIR") - .validator(is_valid_signer) - .takes_value(true) - .index(2) - .help( - "Specify the account keypair. \ - This may be a keypair file or the ASK keyword. \ - [default: associated token account for --owner]" - ), - ) - .arg( - Arg::with_name("immutable") - .long("immutable") - .takes_value(false) - .help( - "Lock the owner of this token account from ever being changed" - ), - ) - .arg(owner_address_arg()) - .nonce_args(true) - ) - .subcommand( - SubCommand::with_name(CommandName::CreateMultisig.into()) - .about("Create a new account describing an M:N multisignature") - .arg( - Arg::with_name("minimum_signers") - .value_name("MINIMUM_SIGNERS") - .validator(is_multisig_minimum_signers) - .takes_value(true) - .index(1) - .required(true) - .help(minimum_signers_help), - ) - .arg( - Arg::with_name("multisig_member") - .value_name("MULTISIG_MEMBER_PUBKEY") - .validator(is_valid_pubkey) - .takes_value(true) - .index(2) - .required(true) - .min_values(MIN_SIGNERS as u64) - .max_values(MAX_SIGNERS as u64) - .help(multisig_member_help), - ) - .arg( - Arg::with_name("address_keypair") - .long("address-keypair") - .value_name("ADDRESS_KEYPAIR") - .validator(is_valid_signer) - .takes_value(true) - .help( - "Specify the address keypair. \ - This may be a keypair file or the ASK keyword. \ - [default: randomly generated keypair]" - ), - ) - .nonce_args(true) - ) - .subcommand( - SubCommand::with_name(CommandName::Authorize.into()) - .about("Authorize a new signing keypair to a token or token account") - .arg( - Arg::with_name("address") - .validator(is_valid_pubkey) - .value_name("TOKEN_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The address of the token mint or account"), - ) - .arg( - Arg::with_name("authority_type") - .value_name("AUTHORITY_TYPE") - .takes_value(true) - .possible_values(&CliAuthorityType::iter().map(Into::into).collect::>()) - .index(2) - .required(true) - .help("The new authority type. \ - Token mints support `mint`, `freeze`, and mint extension authorities; \ - Token accounts support `owner`, `close`, and account extension \ - authorities."), - ) - .arg( - Arg::with_name("new_authority") - .validator(is_valid_pubkey) - .value_name("AUTHORITY_ADDRESS") - .takes_value(true) - .index(3) - .required_unless("disable") - .help("The address of the new authority"), - ) - .arg( - Arg::with_name("authority") - .long("authority") - .alias("owner") - .value_name("KEYPAIR") - .validator(is_valid_signer) - .takes_value(true) - .help( - "Specify the current authority keypair. \ - Defaults to the client keypair." - ), - ) - .arg( - Arg::with_name("disable") - .long("disable") - .takes_value(false) - .conflicts_with("new_authority") - .help("Disable mint, freeze, or close functionality by setting authority to None.") - ) - .arg( - Arg::with_name("force") - .long("force") - .hidden(true) - .help("Force re-authorize the wallet's associate token account. Don't use this flag"), - ) - .arg(multisig_signer_arg()) - .nonce_args(true) - .offline_args(), - ) - .subcommand( - SubCommand::with_name(CommandName::Transfer.into()) - .about("Transfer tokens between accounts") - .arg( - Arg::with_name("token") - .validator(is_valid_pubkey) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("Token to transfer"), - ) - .arg( - Arg::with_name("amount") - .validator(is_amount_or_all) - .value_name("TOKEN_AMOUNT") - .takes_value(true) - .index(2) - .required(true) - .help("Amount to send, in tokens; accepts keyword ALL"), - ) - .arg( - Arg::with_name("recipient") - .validator(is_valid_pubkey) - .value_name("RECIPIENT_WALLET_ADDRESS or RECIPIENT_TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .index(3) - .required(true) - .help("If a token account address is provided, use it as the recipient. \ - Otherwise assume the recipient address is a user wallet and transfer to \ - the associated token account") - ) - .arg( - Arg::with_name("from") - .validator(is_valid_pubkey) - .value_name("SENDER_TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .long("from") - .help("Specify the sending token account \ - [default: owner's associated token account]") - ) - .arg(owner_keypair_arg_with_value_name("SENDER_TOKEN_OWNER_KEYPAIR") - .help( - "Specify the owner of the sending token account. \ - This may be a keypair file or the ASK keyword. \ - Defaults to the client keypair.", - ), - ) - .arg( - Arg::with_name("allow_unfunded_recipient") - .long("allow-unfunded-recipient") - .takes_value(false) - .help("Complete the transfer even if the recipient address is not funded") - ) - .arg( - Arg::with_name("allow_empty_recipient") - .long("allow-empty-recipient") - .takes_value(false) - .hidden(true) // Deprecated, use --allow-unfunded-recipient instead - ) - .arg( - Arg::with_name("fund_recipient") - .long("fund-recipient") - .takes_value(false) - .conflicts_with("confidential") - .help("Create the associated token account for the recipient if doesn't already exist") - ) - .arg( - Arg::with_name("no_wait") - .long("no-wait") - .takes_value(false) - .help("Return signature immediately after submitting the transaction, instead of waiting for confirmations"), - ) - .arg( - Arg::with_name("allow_non_system_account_recipient") - .long("allow-non-system-account-recipient") - .takes_value(false) - .help("Send tokens to the recipient even if the recipient is not a wallet owned by System Program."), - ) - .arg( - Arg::with_name("no_recipient_is_ata_owner") - .long("no-recipient-is-ata-owner") - .takes_value(false) - .requires("sign_only") - .help("In sign-only mode, specifies that the recipient is the owner of the associated token account rather than an actual token account"), - ) - .arg( - Arg::with_name("recipient_is_ata_owner") - .long("recipient-is-ata-owner") - .takes_value(false) - .hidden(true) - .conflicts_with("no_recipient_is_ata_owner") - .requires("sign_only") - .help("recipient-is-ata-owner is now the default behavior. The option has been deprecated and will be removed in a future release."), - ) - .arg( - Arg::with_name("expected_fee") - .long("expected-fee") - .validator(is_amount) - .value_name("TOKEN_AMOUNT") - .takes_value(true) - .help("Expected fee amount collected during the transfer"), - ) - .arg( - Arg::with_name("transfer_hook_account") - .long("transfer-hook-account") - .validator(validate_transfer_hook_account) - .value_name("PUBKEY:ROLE") - .takes_value(true) - .multiple(true) - .min_values(0u64) - .help("Additional pubkey(s) required for a transfer hook and their \ - role, in the format \":\". The role must be \ - \"readonly\", \"writable\". \"readonly-signer\", or \"writable-signer\".\ - Used for offline transaction creation and signing.") - ) - .arg( - Arg::with_name("confidential") - .long("confidential") - .takes_value(false) - .conflicts_with("fund_recipient") - .help("Send tokens confidentially. Both sender and recipient accounts must \ - be pre-configured for confidential transfers.") - ) - .arg(multisig_signer_arg()) - .arg(mint_decimals_arg()) - .nonce_args(true) - .arg(memo_arg()) - .offline_args_config(&SignOnlyNeedsMintDecimals{}), - ) - .subcommand( - SubCommand::with_name(CommandName::Burn.into()) - .about("Burn tokens from an account") - .arg( - Arg::with_name("account") - .validator(is_valid_pubkey) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The token account address to burn from"), - ) - .arg( - Arg::with_name("amount") - .validator(is_amount) - .value_name("TOKEN_AMOUNT") - .takes_value(true) - .index(2) - .required(true) - .help("Amount to burn, in tokens"), - ) - .arg(owner_keypair_arg_with_value_name("TOKEN_OWNER_KEYPAIR") - .help( - "Specify the burnt token owner account. \ - This may be a keypair file or the ASK keyword. \ - Defaults to the client keypair.", - ), - ) - .arg(multisig_signer_arg()) - .mint_args() - .nonce_args(true) - .arg(memo_arg()) - .offline_args_config(&SignOnlyNeedsFullMintSpec{}), - ) - .subcommand( - SubCommand::with_name(CommandName::Mint.into()) - .about("Mint new tokens") - .arg( - Arg::with_name("token") - .validator(is_valid_pubkey) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The token to mint"), - ) - .arg( - Arg::with_name("amount") - .validator(is_amount) - .value_name("TOKEN_AMOUNT") - .takes_value(true) - .index(2) - .required(true) - .help("Amount to mint, in tokens"), - ) - .arg( - Arg::with_name("recipient") - .validator(is_valid_pubkey) - .value_name("RECIPIENT_TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .conflicts_with("recipient_owner") - .index(3) - .help("The token account address of recipient \ - [default: associated token account for --mint-authority]"), - ) - .arg( - Arg::with_name("recipient_owner") - .long("recipient-owner") - .validator(is_valid_pubkey) - .value_name("RECIPIENT_WALLET_ADDRESS") - .takes_value(true) - .conflicts_with("recipient") - .help("The owner of the recipient associated token account"), - ) - .arg( - Arg::with_name("mint_authority") - .long("mint-authority") - .alias("owner") - .value_name("KEYPAIR") - .validator(is_valid_signer) - .takes_value(true) - .help( - "Specify the mint authority keypair. \ - This may be a keypair file or the ASK keyword. \ - Defaults to the client keypair." - ), - ) - .arg(mint_decimals_arg()) - .arg(multisig_signer_arg()) - .nonce_args(true) - .arg(memo_arg()) - .offline_args_config(&SignOnlyNeedsMintDecimals{}), - ) - .subcommand( - SubCommand::with_name(CommandName::Freeze.into()) - .about("Freeze a token account") - .arg( - Arg::with_name("account") - .validator(is_valid_pubkey) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The address of the token account to freeze"), - ) - .arg( - Arg::with_name("freeze_authority") - .long("freeze-authority") - .alias("owner") - .value_name("KEYPAIR") - .validator(is_valid_signer) - .takes_value(true) - .help( - "Specify the freeze authority keypair. \ - This may be a keypair file or the ASK keyword. \ - Defaults to the client keypair." - ), - ) - .arg(mint_address_arg()) - .arg(multisig_signer_arg()) - .nonce_args(true) - .offline_args_config(&SignOnlyNeedsMintAddress{}), - ) - .subcommand( - SubCommand::with_name(CommandName::Thaw.into()) - .about("Thaw a token account") - .arg( - Arg::with_name("account") - .validator(is_valid_pubkey) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The address of the token account to thaw"), - ) - .arg( - Arg::with_name("freeze_authority") - .long("freeze-authority") - .alias("owner") - .value_name("KEYPAIR") - .validator(is_valid_signer) - .takes_value(true) - .help( - "Specify the freeze authority keypair. \ - This may be a keypair file or the ASK keyword. \ - Defaults to the client keypair." - ), - ) - .arg(mint_address_arg()) - .arg(multisig_signer_arg()) - .nonce_args(true) - .offline_args_config(&SignOnlyNeedsMintAddress{}), - ) - .subcommand( - SubCommand::with_name(CommandName::Wrap.into()) - .about("Wrap native SOL in a SOL token account") - .arg( - Arg::with_name("amount") - .validator(is_amount) - .value_name("AMOUNT") - .takes_value(true) - .index(1) - .required(true) - .help("Amount of SOL to wrap"), - ) - .arg( - Arg::with_name("wallet_keypair") - .alias("owner") - .value_name("KEYPAIR") - .validator(is_valid_signer) - .takes_value(true) - .help( - "Specify the keypair for the wallet which will have its native SOL wrapped. \ - This wallet will be assigned as the owner of the wrapped SOL token account. \ - This may be a keypair file or the ASK keyword. \ - Defaults to the client keypair." - ), - ) - .arg( - Arg::with_name("create_aux_account") - .takes_value(false) - .long("create-aux-account") - .help("Wrap SOL in an auxiliary account instead of associated token account"), - ) - .arg( - Arg::with_name("immutable") - .long("immutable") - .takes_value(false) - .help( - "Lock the owner of this token account from ever being changed" - ), - ) - .nonce_args(true) - .offline_args(), - ) - .subcommand( - SubCommand::with_name(CommandName::Unwrap.into()) - .about("Unwrap a SOL token account") - .arg( - Arg::with_name("account") - .validator(is_valid_pubkey) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .index(1) - .help("The address of the auxiliary token account to unwrap \ - [default: associated token account for --owner]"), - ) - .arg( - Arg::with_name("wallet_keypair") - .alias("owner") - .value_name("KEYPAIR") - .validator(is_valid_signer) - .takes_value(true) - .help( - "Specify the keypair for the wallet which owns the wrapped SOL. \ - This wallet will receive the unwrapped SOL. \ - This may be a keypair file or the ASK keyword. \ - Defaults to the client keypair." - ), - ) - .arg(owner_address_arg()) - .arg(multisig_signer_arg()) - .nonce_args(true) - .offline_args(), - ) - .subcommand( - SubCommand::with_name(CommandName::Approve.into()) - .about("Approve a delegate for a token account") - .arg( - Arg::with_name("account") - .validator(is_valid_pubkey) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The address of the token account to delegate"), - ) - .arg( - Arg::with_name("amount") - .validator(is_amount) - .value_name("TOKEN_AMOUNT") - .takes_value(true) - .index(2) - .required(true) - .help("Amount to approve, in tokens"), - ) - .arg( - Arg::with_name("delegate") - .validator(is_valid_pubkey) - .value_name("DELEGATE_TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .index(3) - .required(true) - .help("The token account address of delegate"), - ) - .arg( - owner_keypair_arg() - ) - .arg(multisig_signer_arg()) - .mint_args() - .nonce_args(true) - .offline_args_config(&SignOnlyNeedsFullMintSpec{}), - ) - .subcommand( - SubCommand::with_name(CommandName::Revoke.into()) - .about("Revoke a delegate's authority") - .arg( - Arg::with_name("account") - .validator(is_valid_pubkey) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The address of the token account"), - ) - .arg(owner_keypair_arg() - ) - .arg(delegate_address_arg()) - .arg(multisig_signer_arg()) - .nonce_args(true) - .offline_args_config(&SignOnlyNeedsDelegateAddress{}), - ) - .subcommand( - SubCommand::with_name(CommandName::Close.into()) - .about("Close a token account") - .arg( - Arg::with_name("token") - .validator(is_valid_pubkey) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required_unless("address") - .help("Token of the associated account to close. \ - To close a specific account, use the `--address` parameter instead"), - ) - .arg( - Arg::with_name("recipient") - .long("recipient") - .validator(is_valid_pubkey) - .value_name("REFUND_ACCOUNT_ADDRESS") - .takes_value(true) - .help("The address of the account to receive remaining SOL [default: --owner]"), - ) - .arg( - Arg::with_name("close_authority") - .long("close-authority") - .alias("owner") - .value_name("KEYPAIR") - .validator(is_valid_signer) - .takes_value(true) - .help( - "Specify the token's close authority if it has one, \ - otherwise specify the token's owner keypair. \ - This may be a keypair file or the ASK keyword. \ - Defaults to the client keypair.", - ), - ) - .arg( - Arg::with_name("address") - .long("address") - .validator(is_valid_pubkey) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .conflicts_with("token") - .help("Specify the token account to close \ - [default: owner's associated token account]"), - ) - .arg(owner_address_arg()) - .arg(multisig_signer_arg()) - .nonce_args(true) - ) - .subcommand( - SubCommand::with_name(CommandName::CloseMint.into()) - .about("Close a token mint") - .arg( - Arg::with_name("token") - .validator(is_valid_pubkey) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("Token to close"), - ) - .arg( - Arg::with_name("recipient") - .long("recipient") - .validator(is_valid_pubkey) - .value_name("REFUND_ACCOUNT_ADDRESS") - .takes_value(true) - .help("The address of the account to receive remaining SOL [default: --owner]"), - ) - .arg( - Arg::with_name("close_authority") - .long("close-authority") - .value_name("KEYPAIR") - .validator(is_valid_signer) - .takes_value(true) - .help( - "Specify the token's close authority. \ - This may be a keypair file or the ASK keyword. \ - Defaults to the client keypair.", - ), - ) - .arg(owner_address_arg()) - .arg(multisig_signer_arg()) - .nonce_args(true) - .offline_args(), - ) - .subcommand( - SubCommand::with_name(CommandName::Balance.into()) - .about("Get token account balance") - .arg( - Arg::with_name("token") - .validator(is_valid_pubkey) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required_unless("address") - .help("Token of associated account. To query a specific account, use the `--address` parameter instead"), - ) - .arg(owner_address_arg().conflicts_with("address")) - .arg( - Arg::with_name("address") - .validator(is_valid_pubkey) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .long("address") - .conflicts_with("token") - .help("Specify the token account to query \ - [default: owner's associated token account]"), - ), - ) - .subcommand( - SubCommand::with_name(CommandName::Supply.into()) - .about("Get token supply") - .arg( - Arg::with_name("token") - .validator(is_valid_pubkey) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The token address"), - ), - ) - .subcommand( - SubCommand::with_name(CommandName::Accounts.into()) - .about("List all token accounts by owner") - .arg( - Arg::with_name("token") - .validator(is_valid_pubkey) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .help("Limit results to the given token. [Default: list accounts for all tokens]"), - ) - .arg( - Arg::with_name("delegated") - .long("delegated") - .takes_value(false) - .conflicts_with("externally_closeable") - .help( - "Limit results to accounts with transfer delegations" - ), - ) - .arg( - Arg::with_name("externally_closeable") - .long("externally-closeable") - .takes_value(false) - .conflicts_with("delegated") - .help( - "Limit results to accounts with external close authorities" - ), - ) - .arg( - Arg::with_name("addresses_only") - .long("addresses-only") - .takes_value(false) - .conflicts_with("verbose") - .conflicts_with("output_format") - .help( - "Print token account addresses only" - ), - ) - .arg(owner_address_arg()) - ) - .subcommand( - SubCommand::with_name(CommandName::Address.into()) - .about("Get wallet address") - .arg( - Arg::with_name("token") - .validator(is_valid_pubkey) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .long("token") - .requires("verbose") - .help("Return the associated token address for the given token. \ - [Default: return the client keypair address]") - ) - .arg( - owner_address_arg() - .requires("token") - .help("Return the associated token address for the given owner. \ - [Default: return the associated token address for the client keypair]"), - ), - ) - .subcommand( - SubCommand::with_name(CommandName::AccountInfo.into()) - .about("Query details of an SPL Token account by address (DEPRECATED: use `spl-token display`)") - .setting(AppSettings::Hidden) - .arg( - Arg::with_name("token") - .validator(is_valid_pubkey) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .conflicts_with("address") - .required_unless("address") - .help("Token of associated account. \ - To query a specific account, use the `--address` parameter instead"), - ) - .arg( - Arg::with_name(OWNER_ADDRESS_ARG.name) - .takes_value(true) - .value_name("OWNER_ADDRESS") - .validator(is_valid_signer) - .help(OWNER_ADDRESS_ARG.help) - .index(2) - .conflicts_with("address") - .help("Owner of the associated account for the specified token. \ - To query a specific account, use the `--address` parameter instead. \ - Defaults to the client keypair."), - ) - .arg( - Arg::with_name("address") - .validator(is_valid_pubkey) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .long("address") - .conflicts_with("token") - .help("Specify the token account to query"), - ), - ) - .subcommand( - SubCommand::with_name(CommandName::MultisigInfo.into()) - .about("Query details of an SPL Token multisig account by address (DEPRECATED: use `spl-token display`)") - .setting(AppSettings::Hidden) - .arg( - Arg::with_name("address") - .validator(is_valid_pubkey) - .value_name("MULTISIG_ACCOUNT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The address of the SPL Token multisig account to query"), - ), - ) - .subcommand( - SubCommand::with_name(CommandName::Display.into()) - .about("Query details of an SPL Token mint, account, or multisig by address") - .arg( - Arg::with_name("address") - .validator(is_valid_pubkey) - .value_name("TOKEN_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The address of the SPL Token mint, account, or multisig to query"), - ), - ) - .subcommand( - SubCommand::with_name(CommandName::Gc.into()) - .about("Cleanup unnecessary token accounts") - .arg(owner_keypair_arg()) - .arg( - Arg::with_name("close_empty_associated_accounts") - .long("close-empty-associated-accounts") - .takes_value(false) - .help("close all empty associated token accounts (to get SOL back)") - ) - ) - .subcommand( - SubCommand::with_name(CommandName::SyncNative.into()) - .about("Sync a native SOL token account to its underlying lamports") - .arg( - owner_address_arg() - .index(1) - .conflicts_with("address") - .help("Owner of the associated account for the native token. \ - To query a specific account, use the `--address` parameter instead. \ - Defaults to the client keypair."), - ) - .arg( - Arg::with_name("address") - .validator(is_valid_pubkey) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .long("address") - .conflicts_with("owner") - .help("Specify the specific token account address to sync"), - ), - ) - .subcommand( - SubCommand::with_name(CommandName::EnableRequiredTransferMemos.into()) - .about("Enable required transfer memos for token account") - .arg( - Arg::with_name("account") - .validator(is_valid_pubkey) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The address of the token account to require transfer memos for") - ) - .arg( - owner_address_arg() - ) - .arg(multisig_signer_arg()) - .nonce_args(true) - ) - .subcommand( - SubCommand::with_name(CommandName::DisableRequiredTransferMemos.into()) - .about("Disable required transfer memos for token account") - .arg( - Arg::with_name("account") - .validator(is_valid_pubkey) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The address of the token account to stop requiring transfer memos for"), - ) - .arg( - owner_address_arg() - ) - .arg(multisig_signer_arg()) - .nonce_args(true) - ) - .subcommand( - SubCommand::with_name(CommandName::EnableCpiGuard.into()) - .about("Enable CPI Guard for token account") - .arg( - Arg::with_name("account") - .validator(is_valid_pubkey) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The address of the token account to enable CPI Guard for") - ) - .arg( - owner_address_arg() - ) - .arg(multisig_signer_arg()) - .nonce_args(true) - ) - .subcommand( - SubCommand::with_name(CommandName::DisableCpiGuard.into()) - .about("Disable CPI Guard for token account") - .arg( - Arg::with_name("account") - .validator(is_valid_pubkey) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The address of the token account to disable CPI Guard for"), - ) - .arg( - owner_address_arg() - ) - .arg(multisig_signer_arg()) - .nonce_args(true) - ) - .subcommand( - SubCommand::with_name(CommandName::UpdateDefaultAccountState.into()) - .about("Updates default account state for the mint. Requires the default account state extension.") - .arg( - Arg::with_name("token") - .validator(is_valid_pubkey) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The address of the token mint to update default account state"), - ) - .arg( - Arg::with_name("state") - .value_name("STATE") - .takes_value(true) - .possible_values(&["initialized", "frozen"]) - .index(2) - .required(true) - .help("The new default account state."), - ) - .arg( - Arg::with_name("freeze_authority") - .long("freeze-authority") - .value_name("KEYPAIR") - .validator(is_valid_signer) - .takes_value(true) - .help( - "Specify the token's freeze authority. \ - This may be a keypair file or the ASK keyword. \ - Defaults to the client keypair.", - ), - ) - .arg(owner_address_arg()) - .arg(multisig_signer_arg()) - .nonce_args(true) - .offline_args(), - ) - .subcommand( - SubCommand::with_name(CommandName::UpdateMetadataAddress.into()) - .about("Updates metadata pointer address for the mint. Requires the metadata pointer extension.") - .arg( - Arg::with_name("token") - .validator(is_valid_pubkey) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The address of the token mint to update the metadata pointer address"), - ) - .arg( - Arg::with_name("metadata_address") - .index(2) - .validator(is_valid_pubkey) - .value_name("METADATA_ADDRESS") - .takes_value(true) - .required_unless("disable") - .help("Specify address that stores token's metadata-pointer"), - ) - .arg( - Arg::with_name("disable") - .long("disable") - .takes_value(false) - .conflicts_with("metadata_address") - .help("Unset metadata pointer address.") - ) - .arg( - Arg::with_name("authority") - .long("authority") - .value_name("KEYPAIR") - .validator(is_valid_signer) - .takes_value(true) - .help( - "Specify the token's metadata-pointer authority. \ - This may be a keypair file or the ASK keyword. \ - Defaults to the client keypair.", - ), - ) - .arg(multisig_signer_arg()) - .nonce_args(true) - ) - .subcommand( - SubCommand::with_name(CommandName::WithdrawWithheldTokens.into()) - .about("Withdraw withheld transfer fee tokens from mint and / or account(s)") - .arg( - Arg::with_name("account") - .validator(is_valid_pubkey) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The address of the token account to receive withdrawn tokens"), - ) - .arg( - Arg::with_name("source") - .validator(is_valid_pubkey) - .value_name("ACCOUNT_ADDRESS") - .takes_value(true) - .multiple(true) - .min_values(0u64) - .help("The token accounts to withdraw from") - ) - .arg( - Arg::with_name("include_mint") - .long("include-mint") - .takes_value(false) - .help("Also withdraw withheld tokens from the mint"), - ) - .arg( - Arg::with_name("withdraw_withheld_authority") - .long("withdraw-withheld-authority") - .alias("owner") - .value_name("KEYPAIR") - .validator(is_valid_signer) - .takes_value(true) - .help( - "Specify the withdraw withheld authority keypair. \ - This may be a keypair file or the ASK keyword. \ - Defaults to the client keypair." - ), - ) - .arg(owner_address_arg()) - .arg(multisig_signer_arg()) - ) - .subcommand( - SubCommand::with_name(CommandName::SetTransferFee.into()) - .about("Set the transfer fee for a token with a configured transfer fee") - .arg( - Arg::with_name("token") - .validator(is_valid_pubkey) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .required(true) - .help("The interest-bearing token address"), - ) - .arg( - Arg::with_name("transfer_fee_basis_points") - .value_name("FEE_IN_BASIS_POINTS") - .takes_value(true) - .required(true) - .help("The new transfer fee in basis points"), - ) - .arg( - Arg::with_name("maximum_fee") - .value_name("TOKEN_AMOUNT") - .validator(is_amount) - .takes_value(true) - .required(true) - .help("The new maximum transfer fee in UI amount"), - ) - .arg( - Arg::with_name("transfer_fee_authority") - .long("transfer-fee-authority") - .validator(is_valid_signer) - .value_name("SIGNER") - .takes_value(true) - .help( - "Specify the rate authority keypair. \ - Defaults to the client keypair address." - ) - ) - .arg(mint_decimals_arg()) - .offline_args_config(&SignOnlyNeedsMintDecimals{}) - ) - .subcommand( - SubCommand::with_name(CommandName::WithdrawExcessLamports.into()) - .about("Withdraw lamports from a Token Program owned account") - .arg( - Arg::with_name("from") - .validator(is_valid_pubkey) - .value_name("SOURCE_ACCOUNT_ADDRESS") - .takes_value(true) - .required(true) - .help("Specify the address of the account to recover lamports from"), - ) - .arg( - Arg::with_name("recipient") - .validator(is_valid_pubkey) - .value_name("REFUND_ACCOUNT_ADDRESS") - .takes_value(true) - .required(true) - .help("Specify the address of the account to send lamports to"), - ) - .arg(owner_address_arg()) - .arg(multisig_signer_arg()) - ) - .subcommand( - SubCommand::with_name(CommandName::UpdateConfidentialTransferSettings.into()) - .about("Update confidential transfer configuation for a token") - .arg( - Arg::with_name("token") - .validator(is_valid_pubkey) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The address of the token mint to update confidential transfer configuration for") - ) - .arg( - Arg::with_name("approve_policy") - .long("approve-policy") - .value_name("APPROVE_POLICY") - .takes_value(true) - .possible_values(&["auto", "manual"]) - .help( - "Policy for enabling accounts to make confidential transfers. If \"auto\" \ - is selected, then accounts are automatically approved to make \ - confidential transfers. If \"manual\" is selected, then the \ - confidential transfer mint authority must approve each account \ - before it can make confidential transfers." - ) - ) - .arg( - Arg::with_name("auditor_pubkey") - .long("auditor-pubkey") - .value_name("AUDITOR_PUBKEY") - .takes_value(true) - .help( - "The auditor encryption public key. The corresponding private key for \ - this auditor public key can be used to decrypt all confidential \ - transfers involving tokens from this mint. Currently, the auditor \ - public key can only be specified as a direct *base64* encoding of \ - an ElGamal public key. More methods of specifying the auditor public \ - key will be supported in a future version. To disable auditability \ - feature for the token, use \"none\"." - ) - ) - .group( - ArgGroup::with_name("update_fields").args(&["approve_policy", "auditor_pubkey"]) - .required(true) - .multiple(true) - ) - .arg( - Arg::with_name("confidential_transfer_authority") - .long("confidential-transfer-authority") - .validator(is_valid_signer) - .value_name("SIGNER") - .takes_value(true) - .help( - "Specify the confidential transfer authority keypair. \ - Defaults to the client keypair address." - ) - ) - .nonce_args(true) - .offline_args(), - ) - .subcommand( - SubCommand::with_name(CommandName::ConfigureConfidentialTransferAccount.into()) - .about("Configure confidential transfers for token account") - .arg( - Arg::with_name("token") - .long("token") - .validator(is_valid_pubkey) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required_unless("address") - .help("The token address with confidential transfers enabled"), - ) - .arg( - Arg::with_name("address") - .long("address") - .validator(is_valid_pubkey) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .conflicts_with("token") - .help("The address of the token account to configure confidential transfers for \ - [default: owner's associated token account]") - ) - .arg( - owner_address_arg() - ) - .arg( - Arg::with_name("maximum_pending_balance_credit_counter") - .long("maximum-pending-balance-credit-counter") - .value_name("MAXIMUM-CREDIT-COUNTER") - .takes_value(true) - .help( - "The maximum pending balance credit counter. \ - This parameter limits the number of confidential transfers that a token account \ - can receive to facilitate decryption of the encrypted balance. \ - Defaults to 65536 (2^16)" - ) - ) - .arg(multisig_signer_arg()) - .nonce_args(true) - ) - .subcommand( - SubCommand::with_name(CommandName::EnableConfidentialCredits.into()) - .about("Enable confidential transfers for token account. To enable confidential transfers \ - for the first time, use `configure-confidential-transfer-account` instead.") - .arg( - Arg::with_name("token") - .long("token") - .validator(is_valid_pubkey) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required_unless("address") - .help("The token address with confidential transfers enabled"), - ) - .arg( - Arg::with_name("address") - .long("address") - .validator(is_valid_pubkey) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .conflicts_with("token") - .help("The address of the token account to enable confidential transfers for \ - [default: owner's associated token account]") - ) - .arg( - owner_address_arg() - ) - .arg(multisig_signer_arg()) - .nonce_args(true) - ) - .subcommand( - SubCommand::with_name(CommandName::DisableConfidentialCredits.into()) - .about("Disable confidential transfers for token account") - .arg( - Arg::with_name("token") - .long("token") - .validator(is_valid_pubkey) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required_unless("address") - .help("The token address with confidential transfers enabled"), - ) - .arg( - Arg::with_name("address") - .long("address") - .validator(is_valid_pubkey) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .conflicts_with("token") - .help("The address of the token account to disable confidential transfers for \ - [default: owner's associated token account]") - ) - .arg( - owner_address_arg() - ) - .arg(multisig_signer_arg()) - .nonce_args(true) - ) - .subcommand( - SubCommand::with_name(CommandName::EnableNonConfidentialCredits.into()) - .about("Enable non-confidential transfers for token account.") - .arg( - Arg::with_name("token") - .long("token") - .validator(is_valid_pubkey) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required_unless("address") - .help("The token address with confidential transfers enabled"), - ) - .arg( - Arg::with_name("address") - .long("address") - .validator(is_valid_pubkey) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .conflicts_with("token") - .help("The address of the token account to enable non-confidential transfers for \ - [default: owner's associated token account]") - ) - .arg( - owner_address_arg() - ) - .arg(multisig_signer_arg()) - .nonce_args(true) - ) - .subcommand( - SubCommand::with_name(CommandName::DisableNonConfidentialCredits.into()) - .about("Disable non-confidential transfers for token account") - .arg( - Arg::with_name("token") - .long("token") - .validator(is_valid_pubkey) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required_unless("address") - .help("The token address with confidential transfers enabled"), - ) - .arg( - Arg::with_name("address") - .long("address") - .validator(is_valid_pubkey) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .conflicts_with("token") - .help("The address of the token account to disable non-confidential transfers for \ - [default: owner's associated token account]") - ) - .arg( - owner_address_arg() - ) - .arg(multisig_signer_arg()) - .nonce_args(true) - ) - .subcommand( - SubCommand::with_name(CommandName::DepositConfidentialTokens.into()) - .about("Deposit amounts for confidential transfers") - .arg( - Arg::with_name("token") - .long("token") - .validator(is_valid_pubkey) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The token address with confidential transfers enabled"), - ) - .arg( - Arg::with_name("amount") - .validator(is_amount_or_all) - .value_name("TOKEN_AMOUNT") - .takes_value(true) - .index(2) - .required(true) - .help("Amount to deposit; accepts keyword ALL"), - ) - .arg( - Arg::with_name("address") - .long("address") - .validator(is_valid_pubkey) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .help("The address of the token account to configure confidential transfers for \ - [default: owner's associated token account]") - ) - .arg( - owner_address_arg() - ) - .arg(multisig_signer_arg()) - .arg(mint_decimals_arg()) - .nonce_args(true) - ) - .subcommand( - SubCommand::with_name(CommandName::WithdrawConfidentialTokens.into()) - .about("Withdraw amounts for confidential transfers") - .arg( - Arg::with_name("token") - .long("token") - .validator(is_valid_pubkey) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The token address with confidential transfers enabled"), - ) - .arg( - Arg::with_name("amount") - .validator(is_amount_or_all) - .value_name("TOKEN_AMOUNT") - .takes_value(true) - .index(2) - .required(true) - .help("Amount to deposit; accepts keyword ALL"), - ) - .arg( - Arg::with_name("address") - .long("address") - .validator(is_valid_pubkey) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .help("The address of the token account to configure confidential transfers for \ - [default: owner's associated token account]") - ) - .arg( - owner_address_arg() - ) - .arg(multisig_signer_arg()) - .arg(mint_decimals_arg()) - .nonce_args(true) - ) - .subcommand( - SubCommand::with_name(CommandName::ApplyPendingBalance.into()) - .about("Collect confidential tokens from pending to available balance") - .arg( - Arg::with_name("token") - .long("token") - .validator(is_valid_pubkey) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required_unless("address") - .help("The token address with confidential transfers enabled"), - ) - .arg( - Arg::with_name("address") - .long("address") - .validator(is_valid_pubkey) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .help("The address of the token account to configure confidential transfers for \ - [default: owner's associated token account]") - ) - .arg( - owner_address_arg() - ) - .arg(multisig_signer_arg()) - .nonce_args(true) - ) -} - -#[tokio::main] -async fn main() -> Result<(), Error> { - let default_decimals = format!("{}", spl_token_2022::native_mint::DECIMALS); - let minimum_signers_help = minimum_signers_help_string(); - let multisig_member_help = multisig_member_help_string(); - let app_matches = app( - &default_decimals, - &minimum_signers_help, - &multisig_member_help, - ) - .get_matches(); - - let mut wallet_manager = None; - let mut bulk_signers: Vec> = Vec::new(); - - let (sub_command, sub_matches) = app_matches.subcommand(); - let sub_command = CommandName::from_str(sub_command).unwrap(); - let matches = sub_matches.unwrap(); - - let mut multisigner_ids = Vec::new(); - let config = Config::new( - matches, - &mut wallet_manager, - &mut bulk_signers, - &mut multisigner_ids, - ) - .await; - - solana_logger::setup_with_default("solana=info"); - let result = - process_command(&sub_command, matches, &config, wallet_manager, bulk_signers).await?; - println!("{}", result); - Ok(()) -} - -async fn process_command<'a>( - sub_command: &CommandName, - sub_matches: &ArgMatches<'_>, - config: &Config<'a>, - mut wallet_manager: Option>, - mut bulk_signers: Vec>, -) -> CommandResult { - match (sub_command, sub_matches) { - (CommandName::Bench, arg_matches) => { - bench_process_command( - arg_matches, - config, - std::mem::take(&mut bulk_signers), - &mut wallet_manager, - ) - .await - } - (CommandName::CreateToken, arg_matches) => { - let decimals = value_t_or_exit!(arg_matches, "decimals", u8); - let mint_authority = - config.pubkey_or_default(arg_matches, "mint_authority", &mut wallet_manager)?; - let memo = value_t!(arg_matches, "memo", String).ok(); - let rate_bps = value_t!(arg_matches, "interest_rate", i16).ok(); - let metadata_address = value_t!(arg_matches, "metadata_address", Pubkey).ok(); - - let transfer_fee = arg_matches.values_of("transfer_fee").map(|mut v| { - ( - v.next() - .unwrap() - .parse::() - .unwrap_or_else(print_error_and_exit), - v.next() - .unwrap() - .parse::() - .unwrap_or_else(print_error_and_exit), - ) - }); - - let (token_signer, token) = - get_signer(arg_matches, "token_keypair", &mut wallet_manager) - .unwrap_or_else(new_throwaway_signer); - push_signer_with_dedup(token_signer, &mut bulk_signers); - let default_account_state = - arg_matches - .value_of("default_account_state") - .map(|s| match s { - "initialized" => AccountState::Initialized, - "frozen" => AccountState::Frozen, - _ => unreachable!(), - }); - let transfer_hook_program_id = - pubkey_of_signer(arg_matches, "transfer_hook", &mut wallet_manager).unwrap(); - - let confidential_transfer_auto_approve = arg_matches - .value_of("enable_confidential_transfers") - .map(|b| b == "auto"); - - command_create_token( - config, - decimals, - token, - mint_authority, - arg_matches.is_present("enable_freeze"), - arg_matches.is_present("enable_close"), - arg_matches.is_present("enable_non_transferable"), - arg_matches.is_present("enable_permanent_delegate"), - memo, - metadata_address, - rate_bps, - default_account_state, - transfer_fee, - confidential_transfer_auto_approve, - transfer_hook_program_id, - arg_matches.is_present("enable_metadata"), - bulk_signers, - ) - .await - } - (CommandName::SetInterestRate, arg_matches) => { - let token_pubkey = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) - .unwrap() - .unwrap(); - let rate_bps = value_t_or_exit!(arg_matches, "rate", i16); - let (rate_authority_signer, rate_authority_pubkey) = - config.signer_or_default(arg_matches, "rate_authority", &mut wallet_manager); - let bulk_signers = vec![rate_authority_signer]; - - command_set_interest_rate( - config, - token_pubkey, - rate_authority_pubkey, - rate_bps, - bulk_signers, - ) - .await - } - (CommandName::SetTransferHook, arg_matches) => { - let token_pubkey = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) - .unwrap() - .unwrap(); - let new_program_id = - pubkey_of_signer(arg_matches, "new_program_id", &mut wallet_manager).unwrap(); - let (authority_signer, authority_pubkey) = - config.signer_or_default(arg_matches, "authority", &mut wallet_manager); - let bulk_signers = vec![authority_signer]; - - command_set_transfer_hook_program( - config, - token_pubkey, - authority_pubkey, - new_program_id, - bulk_signers, - ) - .await - } - (CommandName::InitializeMetadata, arg_matches) => { - let token_pubkey = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) - .unwrap() - .unwrap(); - let name = arg_matches.value_of("name").unwrap().to_string(); - let symbol = arg_matches.value_of("symbol").unwrap().to_string(); - let uri = arg_matches.value_of("uri").unwrap().to_string(); - let (mint_authority_signer, mint_authority) = - config.signer_or_default(arg_matches, "mint_authority", &mut wallet_manager); - let bulk_signers = vec![mint_authority_signer]; - let update_authority = - config.pubkey_or_default(arg_matches, "update_authority", &mut wallet_manager)?; - - command_initialize_metadata( - config, - token_pubkey, - update_authority, - mint_authority, - name, - symbol, - uri, - bulk_signers, - ) - .await - } - (CommandName::UpdateMetadata, arg_matches) => { - let token_pubkey = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) - .unwrap() - .unwrap(); - let (authority_signer, authority) = - config.signer_or_default(arg_matches, "authority", &mut wallet_manager); - let field = arg_matches.value_of("field").unwrap(); - let field = match field.to_lowercase().as_str() { - "name" => Field::Name, - "symbol" => Field::Symbol, - "uri" => Field::Uri, - _ => Field::Key(field.to_string()), - }; - let value = arg_matches.value_of("value").map(|v| v.to_string()); - let transfer_lamports = value_of::(arg_matches, TRANSFER_LAMPORTS_ARG.name); - let bulk_signers = vec![authority_signer]; - - command_update_metadata( - config, - token_pubkey, - authority, - field, - value, - transfer_lamports, - bulk_signers, - ) - .await - } - (CommandName::CreateAccount, arg_matches) => { - let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) - .unwrap() - .unwrap(); - - // No need to add a signer when creating an associated token account - let account = get_signer(arg_matches, "account_keypair", &mut wallet_manager).map( - |(signer, account)| { - push_signer_with_dedup(signer, &mut bulk_signers); - account - }, - ); - - let owner = config.pubkey_or_default(arg_matches, "owner", &mut wallet_manager)?; - command_create_account( - config, - token, - owner, - account, - arg_matches.is_present("immutable"), - bulk_signers, - ) - .await - } - (CommandName::CreateMultisig, arg_matches) => { - let minimum_signers = value_of::(arg_matches, "minimum_signers").unwrap(); - let multisig_members = - pubkeys_of_multiple_signers(arg_matches, "multisig_member", &mut wallet_manager) - .unwrap_or_else(print_error_and_exit) - .unwrap(); - if minimum_signers as usize > multisig_members.len() { - eprintln!( - "error: MINIMUM_SIGNERS cannot be greater than the number \ - of MULTISIG_MEMBERs passed" - ); - exit(1); - } - - let (signer, _) = get_signer(arg_matches, "address_keypair", &mut wallet_manager) - .unwrap_or_else(new_throwaway_signer); - - command_create_multisig(config, signer, minimum_signers, multisig_members).await - } - (CommandName::Authorize, arg_matches) => { - let address = pubkey_of_signer(arg_matches, "address", &mut wallet_manager) - .unwrap() - .unwrap(); - let authority_type = arg_matches.value_of("authority_type").unwrap(); - let authority_type = CliAuthorityType::from_str(authority_type)?; - - let (authority_signer, authority) = - config.signer_or_default(arg_matches, "authority", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(authority_signer, &mut bulk_signers); - } - - let new_authority = - pubkey_of_signer(arg_matches, "new_authority", &mut wallet_manager).unwrap(); - let force_authorize = arg_matches.is_present("force"); - command_authorize( - config, - address, - authority_type, - authority, - new_authority, - force_authorize, - bulk_signers, - ) - .await - } - (CommandName::Transfer, arg_matches) => { - let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) - .unwrap() - .unwrap(); - let amount = match arg_matches.value_of("amount").unwrap() { - "ALL" => None, - amount => Some(amount.parse::().unwrap()), - }; - let recipient = pubkey_of_signer(arg_matches, "recipient", &mut wallet_manager) - .unwrap() - .unwrap(); - let sender = pubkey_of_signer(arg_matches, "from", &mut wallet_manager).unwrap(); - - let (owner_signer, owner) = - config.signer_or_default(arg_matches, "owner", &mut wallet_manager); - - let confidential_transfer_args = if arg_matches.is_present("confidential") { - // Deriving ElGamal and AES key from signer. Custom ElGamal and AES keys will be - // supported in the future once upgrading to clap-v3. - // - // NOTE:: Seed bytes are hardcoded to be empty bytes for now. They will be - // updated once custom ElGamal and AES keys are supported. - let sender_elgamal_keypair = - ElGamalKeypair::new_from_signer(&*owner_signer, b"").unwrap(); - let sender_aes_key = AeKey::new_from_signer(&*owner_signer, b"").unwrap(); - - // Sign-only mode is not yet supported for confidential transfers, so set - // recipient and auditor ElGamal public to `None` by default. - Some(ConfidentialTransferArgs { - sender_elgamal_keypair, - sender_aes_key, - recipient_elgamal_pubkey: None, - auditor_elgamal_pubkey: None, - }) - } else { - None - }; - - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(owner_signer, &mut bulk_signers); - } - - let mint_decimals = value_of::(arg_matches, MINT_DECIMALS_ARG.name); - let fund_recipient = arg_matches.is_present("fund_recipient"); - let allow_unfunded_recipient = arg_matches.is_present("allow_empty_recipient") - || arg_matches.is_present("allow_unfunded_recipient"); - - let recipient_is_ata_owner = arg_matches.is_present("recipient_is_ata_owner"); - let no_recipient_is_ata_owner = - arg_matches.is_present("no_recipient_is_ata_owner") || !recipient_is_ata_owner; - if recipient_is_ata_owner { - println_display(config, "recipient-is-ata-owner is now the default behavior. The option has been deprecated and will be removed in a future release.".to_string()); - } - let use_unchecked_instruction = arg_matches.is_present("use_unchecked_instruction"); - let expected_fee = value_of::(arg_matches, "expected_fee"); - let memo = value_t!(arg_matches, "memo", String).ok(); - let transfer_hook_accounts = arg_matches.values_of("transfer_hook_account").map(|v| { - v.into_iter() - .map(|s| parse_transfer_hook_account(s).unwrap()) - .collect::>() - }); - - command_transfer( - config, - token, - amount, - recipient, - sender, - owner, - allow_unfunded_recipient, - fund_recipient, - mint_decimals, - no_recipient_is_ata_owner, - use_unchecked_instruction, - expected_fee, - memo, - bulk_signers, - arg_matches.is_present("no_wait"), - arg_matches.is_present("allow_non_system_account_recipient"), - transfer_hook_accounts, - confidential_transfer_args.as_ref(), - ) - .await - } - (CommandName::Burn, arg_matches) => { - let account = pubkey_of_signer(arg_matches, "account", &mut wallet_manager) - .unwrap() - .unwrap(); - - let (owner_signer, owner) = - config.signer_or_default(arg_matches, "owner", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(owner_signer, &mut bulk_signers); - } - - let amount = value_t_or_exit!(arg_matches, "amount", f64); - let mint_address = - pubkey_of_signer(arg_matches, MINT_ADDRESS_ARG.name, &mut wallet_manager).unwrap(); - let mint_decimals = value_of::(arg_matches, MINT_DECIMALS_ARG.name); - let use_unchecked_instruction = arg_matches.is_present("use_unchecked_instruction"); - let memo = value_t!(arg_matches, "memo", String).ok(); - command_burn( - config, - account, - owner, - amount, - mint_address, - mint_decimals, - use_unchecked_instruction, - memo, - bulk_signers, - ) - .await - } - (CommandName::Mint, arg_matches) => { - let (mint_authority_signer, mint_authority) = - config.signer_or_default(arg_matches, "mint_authority", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(mint_authority_signer, &mut bulk_signers); - } - - let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) - .unwrap() - .unwrap(); - let amount = value_t_or_exit!(arg_matches, "amount", f64); - let mint_decimals = value_of::(arg_matches, MINT_DECIMALS_ARG.name); - let mint_info = config.get_mint_info(&token, mint_decimals).await?; - let recipient = if let Some(address) = - pubkey_of_signer(arg_matches, "recipient", &mut wallet_manager).unwrap() - { - address - } else if let Some(address) = - pubkey_of_signer(arg_matches, "recipient_owner", &mut wallet_manager).unwrap() - { - get_associated_token_address_with_program_id(&address, &token, &config.program_id) - } else { - let owner = config.default_signer()?.pubkey(); - config.associated_token_address_for_token_and_program( - &mint_info.address, - &owner, - &mint_info.program_id, - )? - }; - config.check_account(&recipient, Some(token)).await?; - let use_unchecked_instruction = arg_matches.is_present("use_unchecked_instruction"); - let memo = value_t!(arg_matches, "memo", String).ok(); - command_mint( - config, - token, - amount, - recipient, - mint_info, - mint_authority, - use_unchecked_instruction, - memo, - bulk_signers, - ) - .await - } - (CommandName::Freeze, arg_matches) => { - let (freeze_authority_signer, freeze_authority) = - config.signer_or_default(arg_matches, "freeze_authority", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(freeze_authority_signer, &mut bulk_signers); - } - - let account = pubkey_of_signer(arg_matches, "account", &mut wallet_manager) - .unwrap() - .unwrap(); - let mint_address = - pubkey_of_signer(arg_matches, MINT_ADDRESS_ARG.name, &mut wallet_manager).unwrap(); - command_freeze( - config, - account, - mint_address, - freeze_authority, - bulk_signers, - ) - .await - } - (CommandName::Thaw, arg_matches) => { - let (freeze_authority_signer, freeze_authority) = - config.signer_or_default(arg_matches, "freeze_authority", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(freeze_authority_signer, &mut bulk_signers); - } - - let account = pubkey_of_signer(arg_matches, "account", &mut wallet_manager) - .unwrap() - .unwrap(); - let mint_address = - pubkey_of_signer(arg_matches, MINT_ADDRESS_ARG.name, &mut wallet_manager).unwrap(); - command_thaw( - config, - account, - mint_address, - freeze_authority, - bulk_signers, - ) - .await - } - (CommandName::Wrap, arg_matches) => { - let amount = value_t_or_exit!(arg_matches, "amount", f64); - let account = if arg_matches.is_present("create_aux_account") { - let (signer, account) = new_throwaway_signer(); - bulk_signers.push(signer); - Some(account) - } else { - // No need to add a signer when creating an associated token account - None - }; - - let (wallet_signer, wallet_address) = - config.signer_or_default(arg_matches, "wallet_keypair", &mut wallet_manager); - push_signer_with_dedup(wallet_signer, &mut bulk_signers); - - command_wrap( - config, - amount, - wallet_address, - account, - arg_matches.is_present("immutable"), - bulk_signers, - ) - .await - } - (CommandName::Unwrap, arg_matches) => { - let (wallet_signer, wallet_address) = - config.signer_or_default(arg_matches, "wallet_keypair", &mut wallet_manager); - push_signer_with_dedup(wallet_signer, &mut bulk_signers); - - let account = pubkey_of_signer(arg_matches, "account", &mut wallet_manager).unwrap(); - command_unwrap(config, wallet_address, account, bulk_signers).await - } - (CommandName::Approve, arg_matches) => { - let (owner_signer, owner_address) = - config.signer_or_default(arg_matches, "owner", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(owner_signer, &mut bulk_signers); - } - - let account = pubkey_of_signer(arg_matches, "account", &mut wallet_manager) - .unwrap() - .unwrap(); - let amount = value_t_or_exit!(arg_matches, "amount", f64); - let delegate = pubkey_of_signer(arg_matches, "delegate", &mut wallet_manager) - .unwrap() - .unwrap(); - let mint_address = - pubkey_of_signer(arg_matches, MINT_ADDRESS_ARG.name, &mut wallet_manager).unwrap(); - let mint_decimals = value_of::(arg_matches, MINT_DECIMALS_ARG.name); - let use_unchecked_instruction = arg_matches.is_present("use_unchecked_instruction"); - command_approve( - config, - account, - owner_address, - amount, - delegate, - mint_address, - mint_decimals, - use_unchecked_instruction, - bulk_signers, - ) - .await - } - (CommandName::Revoke, arg_matches) => { - let (owner_signer, owner_address) = - config.signer_or_default(arg_matches, "owner", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(owner_signer, &mut bulk_signers); - } - - let account = pubkey_of_signer(arg_matches, "account", &mut wallet_manager) - .unwrap() - .unwrap(); - let delegate_address = - pubkey_of_signer(arg_matches, DELEGATE_ADDRESS_ARG.name, &mut wallet_manager) - .unwrap(); - command_revoke( - config, - account, - owner_address, - delegate_address, - bulk_signers, - ) - .await - } - (CommandName::Close, arg_matches) => { - let (close_authority_signer, close_authority) = - config.signer_or_default(arg_matches, "close_authority", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(close_authority_signer, &mut bulk_signers); - } - - let address = config - .associated_token_address_or_override(arg_matches, "address", &mut wallet_manager) - .await?; - let recipient = - config.pubkey_or_default(arg_matches, "recipient", &mut wallet_manager)?; - command_close(config, address, close_authority, recipient, bulk_signers).await - } - (CommandName::CloseMint, arg_matches) => { - let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) - .unwrap() - .unwrap(); - let (close_authority_signer, close_authority) = - config.signer_or_default(arg_matches, "close_authority", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(close_authority_signer, &mut bulk_signers); - } - let recipient = - config.pubkey_or_default(arg_matches, "recipient", &mut wallet_manager)?; - - command_close_mint(config, token, close_authority, recipient, bulk_signers).await - } - (CommandName::Balance, arg_matches) => { - let address = config - .associated_token_address_or_override(arg_matches, "address", &mut wallet_manager) - .await?; - command_balance(config, address).await - } - (CommandName::Supply, arg_matches) => { - let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) - .unwrap() - .unwrap(); - command_supply(config, token).await - } - (CommandName::Accounts, arg_matches) => { - let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager).unwrap(); - let owner = config.pubkey_or_default(arg_matches, "owner", &mut wallet_manager)?; - let filter = if arg_matches.is_present("delegated") { - AccountFilter::Delegated - } else if arg_matches.is_present("externally_closeable") { - AccountFilter::ExternallyCloseable - } else { - AccountFilter::All - }; - - command_accounts( - config, - token, - owner, - filter, - arg_matches.is_present("addresses_only"), - ) - .await - } - (CommandName::Address, arg_matches) => { - let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager).unwrap(); - let owner = config.pubkey_or_default(arg_matches, "owner", &mut wallet_manager)?; - command_address(config, token, owner).await - } - (CommandName::AccountInfo, arg_matches) => { - let address = config - .associated_token_address_or_override(arg_matches, "address", &mut wallet_manager) - .await?; - command_display(config, address).await - } - (CommandName::MultisigInfo, arg_matches) => { - let address = pubkey_of_signer(arg_matches, "address", &mut wallet_manager) - .unwrap() - .unwrap(); - command_display(config, address).await - } - (CommandName::Display, arg_matches) => { - let address = pubkey_of_signer(arg_matches, "address", &mut wallet_manager) - .unwrap() - .unwrap(); - command_display(config, address).await - } - (CommandName::Gc, arg_matches) => { - match config.output_format { - OutputFormat::Json | OutputFormat::JsonCompact => { - eprintln!( - "`spl-token gc` does not support the `--ouput` parameter at this time" - ); - exit(1); - } - _ => {} - } - - let close_empty_associated_accounts = - arg_matches.is_present("close_empty_associated_accounts"); - - let (owner_signer, owner_address) = - config.signer_or_default(arg_matches, "owner", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(owner_signer, &mut bulk_signers); - } - - command_gc( - config, - owner_address, - close_empty_associated_accounts, - bulk_signers, - ) - .await - } - (CommandName::SyncNative, arg_matches) => { - let native_mint = *native_token_client_from_config(config)?.get_address(); - let address = config - .associated_token_address_for_token_or_override( - arg_matches, - "address", - &mut wallet_manager, - Some(native_mint), - ) - .await; - command_sync_native(config, address?).await - } - (CommandName::EnableRequiredTransferMemos, arg_matches) => { - let (owner_signer, owner) = - config.signer_or_default(arg_matches, "owner", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(owner_signer, &mut bulk_signers); - } - // Since account is required argument it will always be present - let token_account = - config.pubkey_or_default(arg_matches, "account", &mut wallet_manager)?; - command_required_transfer_memos(config, token_account, owner, bulk_signers, true).await - } - (CommandName::DisableRequiredTransferMemos, arg_matches) => { - let (owner_signer, owner) = - config.signer_or_default(arg_matches, "owner", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(owner_signer, &mut bulk_signers); - } - // Since account is required argument it will always be present - let token_account = - config.pubkey_or_default(arg_matches, "account", &mut wallet_manager)?; - command_required_transfer_memos(config, token_account, owner, bulk_signers, false).await - } - (CommandName::EnableCpiGuard, arg_matches) => { - let (owner_signer, owner) = - config.signer_or_default(arg_matches, "owner", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(owner_signer, &mut bulk_signers); - } - // Since account is required argument it will always be present - let token_account = - config.pubkey_or_default(arg_matches, "account", &mut wallet_manager)?; - command_cpi_guard(config, token_account, owner, bulk_signers, true).await - } - (CommandName::DisableCpiGuard, arg_matches) => { - let (owner_signer, owner) = - config.signer_or_default(arg_matches, "owner", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(owner_signer, &mut bulk_signers); - } - // Since account is required argument it will always be present - let token_account = - config.pubkey_or_default(arg_matches, "account", &mut wallet_manager)?; - command_cpi_guard(config, token_account, owner, bulk_signers, false).await - } - (CommandName::UpdateDefaultAccountState, arg_matches) => { - // Since account is required argument it will always be present - let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) - .unwrap() - .unwrap(); - let (freeze_authority_signer, freeze_authority) = - config.signer_or_default(arg_matches, "freeze_authority", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(freeze_authority_signer, &mut bulk_signers); - } - let new_default_state = arg_matches.value_of("state").unwrap(); - let new_default_state = match new_default_state { - "initialized" => AccountState::Initialized, - "frozen" => AccountState::Frozen, - _ => unreachable!(), - }; - command_update_default_account_state( - config, - token, - freeze_authority, - new_default_state, - bulk_signers, - ) - .await - } - (CommandName::UpdateMetadataAddress, arg_matches) => { - // Since account is required argument it will always be present - let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) - .unwrap() - .unwrap(); - - let (authority_signer, authority) = - config.signer_or_default(arg_matches, "authority", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(authority_signer, &mut bulk_signers); - } - let metadata_address = value_t!(arg_matches, "metadata_address", Pubkey).ok(); - - command_update_metadata_pointer_address( - config, - token, - authority, - metadata_address, - bulk_signers, - ) - .await - } - (CommandName::WithdrawWithheldTokens, arg_matches) => { - let (authority_signer, authority) = config.signer_or_default( - arg_matches, - "withdraw_withheld_authority", - &mut wallet_manager, - ); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(authority_signer, &mut bulk_signers); - } - // Since destination is required it will always be present - let destination_token_account = - pubkey_of_signer(arg_matches, "account", &mut wallet_manager) - .unwrap() - .unwrap(); - let include_mint = arg_matches.is_present("include_mint"); - let source_accounts = arg_matches - .values_of("source") - .unwrap_or_default() - .map(|s| Pubkey::from_str(s).unwrap_or_else(print_error_and_exit)) - .collect::>(); - command_withdraw_withheld_tokens( - config, - destination_token_account, - source_accounts, - authority, - include_mint, - bulk_signers, - ) - .await - } - (CommandName::SetTransferFee, arg_matches) => { - let token_pubkey = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) - .unwrap() - .unwrap(); - let transfer_fee_basis_points = - value_t_or_exit!(arg_matches, "transfer_fee_basis_points", u16); - let maximum_fee = value_t_or_exit!(arg_matches, "maximum_fee", f64); - let (transfer_fee_authority_signer, transfer_fee_authority_pubkey) = config - .signer_or_default(arg_matches, "transfer_fee_authority", &mut wallet_manager); - let mint_decimals = value_of::(arg_matches, MINT_DECIMALS_ARG.name); - let bulk_signers = vec![transfer_fee_authority_signer]; - - command_set_transfer_fee( - config, - token_pubkey, - transfer_fee_authority_pubkey, - transfer_fee_basis_points, - maximum_fee, - mint_decimals, - bulk_signers, - ) - .await - } - (CommandName::WithdrawExcessLamports, arg_matches) => { - let (signer, authority) = - config.signer_or_default(arg_matches, "owner", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(signer, &mut bulk_signers); - } - - let source = config.pubkey_or_default(arg_matches, "from", &mut wallet_manager)?; - let destination = - config.pubkey_or_default(arg_matches, "recipient", &mut wallet_manager)?; - - command_withdraw_excess_lamports(config, source, destination, authority, bulk_signers) - .await - } - (CommandName::UpdateConfidentialTransferSettings, arg_matches) => { - let token_pubkey = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) - .unwrap() - .unwrap(); - - let auto_approve = arg_matches.value_of("approve_policy").map(|b| b == "auto"); - - let auditor_encryption_pubkey = if arg_matches.is_present("auditor_pubkey") { - Some(elgamal_pubkey_or_none(arg_matches, "auditor_pubkey")?) - } else { - None - }; - - let (authority_signer, authority_pubkey) = config.signer_or_default( - arg_matches, - "confidential_transfer_authority", - &mut wallet_manager, - ); - let bulk_signers = vec![authority_signer]; - - command_update_confidential_transfer_settings( - config, - token_pubkey, - authority_pubkey, - auto_approve, - auditor_encryption_pubkey, - bulk_signers, - ) - .await - } - (CommandName::ConfigureConfidentialTransferAccount, arg_matches) => { - let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager).unwrap(); - - let (owner_signer, owner) = - config.signer_or_default(arg_matches, "owner", &mut wallet_manager); - - let account = pubkey_of_signer(arg_matches, "address", &mut wallet_manager).unwrap(); - - // Deriving ElGamal and AES key from signer. Custom ElGamal and AES keys will be - // supported in the future once upgrading to clap-v3. - // - // NOTE:: Seed bytes are hardcoded to be empty bytes for now. They will be - // updated once custom ElGamal and AES keys are supported. - let elgamal_keypair = ElGamalKeypair::new_from_signer(&*owner_signer, b"").unwrap(); - let aes_key = AeKey::new_from_signer(&*owner_signer, b"").unwrap(); - - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(owner_signer, &mut bulk_signers); - } - - let maximum_credit_counter = - if arg_matches.is_present("maximum_pending_balance_credit_counter") { - let maximum_credit_counter = value_t_or_exit!( - arg_matches.value_of("maximum_pending_balance_credit_counter"), - u64 - ); - Some(maximum_credit_counter) - } else { - None - }; - - command_configure_confidential_transfer_account( - config, - token, - owner, - account, - maximum_credit_counter, - &elgamal_keypair, - &aes_key, - bulk_signers, - ) - .await - } - (c @ CommandName::EnableConfidentialCredits, arg_matches) - | (c @ CommandName::DisableConfidentialCredits, arg_matches) - | (c @ CommandName::EnableNonConfidentialCredits, arg_matches) - | (c @ CommandName::DisableNonConfidentialCredits, arg_matches) => { - let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager).unwrap(); - - let (owner_signer, owner) = - config.signer_or_default(arg_matches, "owner", &mut wallet_manager); - - let account = pubkey_of_signer(arg_matches, "address", &mut wallet_manager).unwrap(); - - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(owner_signer, &mut bulk_signers); - } - - let (allow_confidential_credits, allow_non_confidential_credits) = match c { - CommandName::EnableConfidentialCredits => (Some(true), None), - CommandName::DisableConfidentialCredits => (Some(false), None), - CommandName::EnableNonConfidentialCredits => (None, Some(true)), - CommandName::DisableNonConfidentialCredits => (None, Some(false)), - _ => (None, None), - }; - - command_enable_disable_confidential_transfers( - config, - token, - owner, - account, - bulk_signers, - allow_confidential_credits, - allow_non_confidential_credits, - ) - .await - } - (c @ CommandName::DepositConfidentialTokens, arg_matches) - | (c @ CommandName::WithdrawConfidentialTokens, arg_matches) => { - let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) - .unwrap() - .unwrap(); - let amount = match arg_matches.value_of("amount").unwrap() { - "ALL" => None, - amount => Some(amount.parse::().unwrap()), - }; - let account = pubkey_of_signer(arg_matches, "address", &mut wallet_manager).unwrap(); - - let (owner_signer, owner) = - config.signer_or_default(arg_matches, "owner", &mut wallet_manager); - - let mint_decimals = value_of::(arg_matches, MINT_DECIMALS_ARG.name); - - let (instruction_type, elgamal_keypair, aes_key) = match c { - CommandName::DepositConfidentialTokens => { - (ConfidentialInstructionType::Deposit, None, None) - } - CommandName::WithdrawConfidentialTokens => { - // Deriving ElGamal and AES key from signer. Custom ElGamal and AES keys will be - // supported in the future once upgrading to clap-v3. - // - // NOTE:: Seed bytes are hardcoded to be empty bytes for now. They will be - // updated once custom ElGamal and AES keys are supported. - let elgamal_keypair = - ElGamalKeypair::new_from_signer(&*owner_signer, b"").unwrap(); - let aes_key = AeKey::new_from_signer(&*owner_signer, b"").unwrap(); - - ( - ConfidentialInstructionType::Withdraw, - Some(elgamal_keypair), - Some(aes_key), - ) - } - _ => panic!("Instruction not supported"), - }; - - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(owner_signer, &mut bulk_signers); - } - - command_deposit_withdraw_confidential_tokens( - config, - token, - owner, - account, - bulk_signers, - amount, - mint_decimals, - instruction_type, - elgamal_keypair.as_ref(), - aes_key.as_ref(), - ) - .await - } - (CommandName::ApplyPendingBalance, arg_matches) => { - let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager).unwrap(); - - let (owner_signer, owner) = - config.signer_or_default(arg_matches, "owner", &mut wallet_manager); - - let account = pubkey_of_signer(arg_matches, "address", &mut wallet_manager).unwrap(); - - // Deriving ElGamal and AES key from signer. Custom ElGamal and AES keys will be - // supported in the future once upgrading to clap-v3. - // - // NOTE:: Seed bytes are hardcoded to be empty bytes for now. They will be - // updated once custom ElGamal and AES keys are supported. - let elgamal_keypair = ElGamalKeypair::new_from_signer(&*owner_signer, b"").unwrap(); - let aes_key = AeKey::new_from_signer(&*owner_signer, b"").unwrap(); - - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(owner_signer, &mut bulk_signers); - } - - command_apply_pending_balance( - config, - token, - owner, - account, - bulk_signers, - &elgamal_keypair, - &aes_key, - ) - .await - } - } -} - -fn format_output(command_output: T, command_name: &CommandName, config: &Config) -> String -where - T: Serialize + Display + QuietDisplay + VerboseDisplay, -{ - config.output_format.formatted_string(&CommandOutput { - command_name: command_name.to_string(), - command_output, - }) -} -enum TransactionReturnData { - CliSignature(CliSignature), - CliSignOnlyData(CliSignOnlyData), -} - -async fn finish_tx<'a>( - config: &Config<'a>, - rpc_response: &RpcClientResponse, - no_wait: bool, -) -> Result { - match rpc_response { - RpcClientResponse::Transaction(transaction) => { - Ok(TransactionReturnData::CliSignOnlyData(return_signers_data( - transaction, - &ReturnSignersConfig { - dump_transaction_message: config.dump_transaction_message, - }, - ))) - } - RpcClientResponse::Signature(signature) if no_wait => { - Ok(TransactionReturnData::CliSignature(CliSignature { - signature: signature.to_string(), - })) - } - RpcClientResponse::Signature(signature) => { - let blockhash = config.program_client.get_latest_blockhash().await?; - config - .rpc_client - .confirm_transaction_with_spinner( - signature, - &blockhash, - config.rpc_client.commitment(), - ) - .await?; - - Ok(TransactionReturnData::CliSignature(CliSignature { - signature: signature.to_string(), - })) - } - RpcClientResponse::Simulation(_) => { - // Implement this once the CLI supports dry-running / simulation - unreachable!() - } - } -} - -#[cfg(test)] -mod tests { - use { - super::*, - serial_test::serial, - solana_sdk::{ - bpf_loader_upgradeable, feature_set, - hash::Hash, - program_pack::Pack, - signature::{write_keypair_file, Keypair, Signer}, - system_instruction, - transaction::Transaction, - }, - solana_test_validator::{TestValidator, TestValidatorGenesis, UpgradeableProgramInfo}, - spl_token_2022::{extension::non_transferable::NonTransferable, state::Multisig}, - spl_token_client::client::{ - ProgramClient, ProgramOfflineClient, ProgramRpcClient, ProgramRpcClientSendTransaction, - }, - std::path::PathBuf, - tempfile::NamedTempFile, - }; - - fn clone_keypair(keypair: &Keypair) -> Keypair { - Keypair::from_bytes(&keypair.to_bytes()).unwrap() - } - - const TEST_DECIMALS: u8 = 0; - - async fn new_validator_for_test() -> (TestValidator, Keypair) { - solana_logger::setup(); - let mut test_validator_genesis = TestValidatorGenesis::default(); - test_validator_genesis.add_upgradeable_programs_with_path(&[ - UpgradeableProgramInfo { - program_id: spl_token::id(), - loader: bpf_loader_upgradeable::id(), - program_path: PathBuf::from("../../target/deploy/spl_token.so"), - upgrade_authority: Pubkey::new_unique(), - }, - UpgradeableProgramInfo { - program_id: spl_associated_token_account::id(), - loader: bpf_loader_upgradeable::id(), - program_path: PathBuf::from("../../target/deploy/spl_associated_token_account.so"), - upgrade_authority: Pubkey::new_unique(), - }, - UpgradeableProgramInfo { - program_id: spl_token_2022::id(), - loader: bpf_loader_upgradeable::id(), - program_path: PathBuf::from("../../target/deploy/spl_token_2022.so"), - upgrade_authority: Pubkey::new_unique(), - }, - ]); - // TODO Remove this once the Range Proof cost goes under 200k compute units - test_validator_genesis - .deactivate_features(&[feature_set::native_programs_consume_cu::id()]); - test_validator_genesis.start_async().await - } - - fn test_config_with_default_signer<'a>( - test_validator: &TestValidator, - payer: &Keypair, - program_id: &Pubkey, - ) -> Config<'a> { - let websocket_url = test_validator.rpc_pubsub_url(); - let rpc_client = Arc::new(test_validator.get_async_rpc_client()); - let program_client: Arc> = Arc::new( - ProgramRpcClient::new(rpc_client.clone(), ProgramRpcClientSendTransaction), - ); - Config { - rpc_client, - program_client, - websocket_url, - output_format: OutputFormat::JsonCompact, - fee_payer: Some(Arc::new(clone_keypair(payer))), - default_signer: Some(Arc::new(clone_keypair(payer))), - nonce_account: None, - nonce_authority: None, - nonce_blockhash: None, - sign_only: false, - dump_transaction_message: false, - multisigner_pubkeys: vec![], - program_id: *program_id, - restrict_to_program_id: true, - } - } - - fn test_config_without_default_signer<'a>( - test_validator: &TestValidator, - program_id: &Pubkey, - ) -> Config<'a> { - let websocket_url = test_validator.rpc_pubsub_url(); - let rpc_client = Arc::new(test_validator.get_async_rpc_client()); - let program_client: Arc> = Arc::new( - ProgramRpcClient::new(rpc_client.clone(), ProgramRpcClientSendTransaction), - ); - Config { - rpc_client, - program_client, - websocket_url, - output_format: OutputFormat::JsonCompact, - fee_payer: None, - default_signer: None, - nonce_account: None, - nonce_authority: None, - nonce_blockhash: None, - sign_only: false, - dump_transaction_message: false, - multisigner_pubkeys: vec![], - program_id: *program_id, - restrict_to_program_id: true, - } - } - - async fn create_nonce(config: &Config<'_>, authority: &Keypair) -> Pubkey { - let nonce = Keypair::new(); - - let nonce_rent = config - .rpc_client - .get_minimum_balance_for_rent_exemption(solana_sdk::nonce::State::size()) - .await - .unwrap(); - let instr = system_instruction::create_nonce_account( - &authority.pubkey(), - &nonce.pubkey(), - &authority.pubkey(), // Make the fee payer the nonce account authority - nonce_rent, - ); - - let blockhash = config.rpc_client.get_latest_blockhash().await.unwrap(); - let tx = Transaction::new_signed_with_payer( - &instr, - Some(&authority.pubkey()), - &[&nonce, authority], - blockhash, - ); - - config - .rpc_client - .send_and_confirm_transaction(&tx) - .await - .unwrap(); - nonce.pubkey() - } - - async fn do_create_native_mint(config: &Config<'_>, program_id: &Pubkey, payer: &Keypair) { - if program_id == &spl_token_2022::id() { - let native_mint = spl_token_2022::native_mint::id(); - if config.rpc_client.get_account(&native_mint).await.is_err() { - let transaction = Transaction::new_signed_with_payer( - &[create_native_mint(program_id, &payer.pubkey()).unwrap()], - Some(&payer.pubkey()), - &[payer], - config.rpc_client.get_latest_blockhash().await.unwrap(), - ); - config - .rpc_client - .send_and_confirm_transaction(&transaction) - .await - .unwrap(); - } - } - } - - async fn create_token(config: &Config<'_>, payer: &Keypair) -> Pubkey { - let token = Keypair::new(); - let token_pubkey = token.pubkey(); - let bulk_signers: Vec> = - vec![Arc::new(clone_keypair(payer)), Arc::new(token)]; - - command_create_token( - config, - TEST_DECIMALS, - token_pubkey, - payer.pubkey(), - false, - false, - false, - false, - None, - None, - None, - None, - None, - None, - None, - false, - bulk_signers, - ) - .await - .unwrap(); - token_pubkey - } - - async fn create_interest_bearing_token( - config: &Config<'_>, - payer: &Keypair, - rate_bps: i16, - ) -> Pubkey { - let token = Keypair::new(); - let token_pubkey = token.pubkey(); - let bulk_signers: Vec> = - vec![Arc::new(clone_keypair(payer)), Arc::new(token)]; - - command_create_token( - config, - TEST_DECIMALS, - token_pubkey, - payer.pubkey(), - false, - false, - false, - false, - None, - None, - Some(rate_bps), - None, - None, - None, - None, - false, - bulk_signers, - ) - .await - .unwrap(); - token_pubkey - } - - async fn create_auxiliary_account( - config: &Config<'_>, - payer: &Keypair, - mint: Pubkey, - ) -> Pubkey { - let auxiliary = Keypair::new(); - let address = auxiliary.pubkey(); - let bulk_signers: Vec> = - vec![Arc::new(clone_keypair(payer)), Arc::new(auxiliary)]; - command_create_account( - config, - mint, - payer.pubkey(), - Some(address), - false, - bulk_signers, - ) - .await - .unwrap(); - address - } - - async fn create_associated_account( - config: &Config<'_>, - payer: &Keypair, - mint: &Pubkey, - owner: &Pubkey, - ) -> Pubkey { - let bulk_signers: Vec> = vec![Arc::new(clone_keypair(payer))]; - command_create_account(config, *mint, *owner, None, false, bulk_signers) - .await - .unwrap(); - get_associated_token_address_with_program_id(owner, mint, &config.program_id) - } - - async fn mint_tokens( - config: &Config<'_>, - payer: &Keypair, - mint: Pubkey, - ui_amount: f64, - recipient: Pubkey, - ) { - let bulk_signers: Vec> = vec![Arc::new(clone_keypair(payer))]; - command_mint( - config, - mint, - ui_amount, - recipient, - MintInfo { - program_id: config.program_id, - address: mint, - decimals: TEST_DECIMALS, - }, - payer.pubkey(), - false, - None, - bulk_signers, - ) - .await - .unwrap(); - } - - async fn process_test_command( - config: &Config<'_>, - payer: &Keypair, - args: &[&str], - ) -> CommandResult { - let default_decimals = format!("{}", spl_token_2022::native_mint::DECIMALS); - let minimum_signers_help = minimum_signers_help_string(); - let multisig_member_help = multisig_member_help_string(); - - let app_matches = app( - &default_decimals, - &minimum_signers_help, - &multisig_member_help, - ) - .get_matches_from(args); - let (sub_command, sub_matches) = app_matches.subcommand(); - let sub_command = CommandName::from_str(sub_command).unwrap(); - let matches = sub_matches.unwrap(); - - let wallet_manager = None; - let bulk_signers: Vec> = vec![Arc::new(clone_keypair(payer))]; - process_command(&sub_command, matches, config, wallet_manager, bulk_signers).await - } - - async fn exec_test_cmd(config: &Config<'_>, args: &[&str]) -> CommandResult { - let default_decimals = format!("{}", spl_token_2022::native_mint::DECIMALS); - let minimum_signers_help = minimum_signers_help_string(); - let multisig_member_help = multisig_member_help_string(); - - let app_matches = app( - &default_decimals, - &minimum_signers_help, - &multisig_member_help, - ) - .get_matches_from(args); - let (sub_command, sub_matches) = app_matches.subcommand(); - let sub_command = CommandName::from_str(sub_command).unwrap(); - let matches = sub_matches.unwrap(); - - let mut wallet_manager = None; - let mut bulk_signers: Vec> = Vec::new(); - let mut multisigner_ids = Vec::new(); - - let config = Config::new_with_clients_and_ws_url( - matches, - &mut wallet_manager, - &mut bulk_signers, - &mut multisigner_ids, - config.rpc_client.clone(), - config.program_client.clone(), - config.websocket_url.clone(), - ) - .await; - - process_command(&sub_command, matches, &config, wallet_manager, bulk_signers).await - } - - #[tokio::test] - #[serial] - async fn create_token_default() { - let (test_validator, payer) = new_validator_for_test().await; - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(&test_validator, &payer, program_id); - let result = process_test_command( - &config, - &payer, - &["spl-token", CommandName::CreateToken.into()], - ) - .await; - let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); - let mint = - Pubkey::from_str(value["commandOutput"]["address"].as_str().unwrap()).unwrap(); - let account = config.rpc_client.get_account(&mint).await.unwrap(); - assert_eq!(account.owner, *program_id); - } - } - - #[tokio::test] - #[serial] - async fn create_token_interest_bearing() { - let (test_validator, payer) = new_validator_for_test().await; - let config = - test_config_with_default_signer(&test_validator, &payer, &spl_token_2022::id()); - let rate_bps: i16 = 100; - let result = process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::CreateToken.into(), - "--interest-rate", - &rate_bps.to_string(), - ], - ) - .await; - let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); - let mint = Pubkey::from_str(value["commandOutput"]["address"].as_str().unwrap()).unwrap(); - let account = config.rpc_client.get_account(&mint).await.unwrap(); - let mint_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = mint_account - .get_extension::() - .unwrap(); - assert_eq!(account.owner, spl_token_2022::id()); - assert_eq!(i16::from(extension.current_rate), rate_bps); - assert_eq!( - Option::::from(extension.rate_authority), - Some(payer.pubkey()) - ); - } - - #[tokio::test] - #[serial] - async fn set_interest_rate() { - let (test_validator, payer) = new_validator_for_test().await; - let config = - test_config_with_default_signer(&test_validator, &payer, &spl_token_2022::id()); - let initial_rate: i16 = 100; - let new_rate: i16 = 300; - let token = create_interest_bearing_token(&config, &payer, initial_rate).await; - let account = config.rpc_client.get_account(&token).await.unwrap(); - let mint_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = mint_account - .get_extension::() - .unwrap(); - assert_eq!(account.owner, spl_token_2022::id()); - assert_eq!(i16::from(extension.current_rate), initial_rate); - - let result = process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::SetInterestRate.into(), - &token.to_string(), - &new_rate.to_string(), - ], - ) - .await; - let _value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); - let account = config.rpc_client.get_account(&token).await.unwrap(); - let mint_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = mint_account - .get_extension::() - .unwrap(); - assert_eq!(i16::from(extension.current_rate), new_rate); - } - - #[tokio::test] - #[serial] - async fn supply() { - let (test_validator, payer) = new_validator_for_test().await; - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(&test_validator, &payer, program_id); - let token = create_token(&config, &payer).await; - let result = process_test_command( - &config, - &payer, - &["spl-token", CommandName::Supply.into(), &token.to_string()], - ) - .await; - let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); - assert_eq!(value["amount"], "0"); - assert_eq!(value["uiAmountString"], "0"); - } - } - - #[tokio::test] - #[serial] - async fn create_account_default() { - let (test_validator, payer) = new_validator_for_test().await; - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(&test_validator, &payer, program_id); - let token = create_token(&config, &payer).await; - let result = process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::CreateAccount.into(), - &token.to_string(), - ], - ) - .await; - result.unwrap(); - } - } - - #[tokio::test] - #[serial] - async fn account_info() { - let (test_validator, payer) = new_validator_for_test().await; - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(&test_validator, &payer, program_id); - let token = create_token(&config, &payer).await; - let _account = - create_associated_account(&config, &payer, &token, &payer.pubkey()).await; - let result = process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::AccountInfo.into(), - &token.to_string(), - ], - ) - .await; - let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); - let account = get_associated_token_address_with_program_id( - &payer.pubkey(), - &token, - &config.program_id, - ); - assert_eq!(value["address"], account.to_string()); - assert_eq!(value["mint"], token.to_string()); - assert_eq!(value["isAssociated"], true); - assert_eq!(value["isNative"], false); - assert_eq!(value["owner"], payer.pubkey().to_string()); - assert_eq!(value["state"], "initialized"); - } - } - - #[tokio::test] - #[serial] - async fn balance() { - let (test_validator, payer) = new_validator_for_test().await; - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(&test_validator, &payer, program_id); - let token = create_token(&config, &payer).await; - let _account = - create_associated_account(&config, &payer, &token, &payer.pubkey()).await; - let result = process_test_command( - &config, - &payer, - &["spl-token", CommandName::Balance.into(), &token.to_string()], - ) - .await; - let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); - assert_eq!(value["amount"], "0"); - assert_eq!(value["uiAmountString"], "0"); - } - } - - #[tokio::test] - #[serial] - async fn mint() { - let (test_validator, payer) = new_validator_for_test().await; - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(&test_validator, &payer, program_id); - let token = create_token(&config, &payer).await; - let account = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; - let mut amount = 0; - - // mint via implicit owner - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Mint.into(), - &token.to_string(), - "1", - ], - ) - .await - .unwrap(); - amount += 1; - - let account_data = config.rpc_client.get_account(&account).await.unwrap(); - let token_account = - StateWithExtensionsOwned::::unpack(account_data.data).unwrap(); - assert_eq!(token_account.base.amount, amount); - assert_eq!(token_account.base.mint, token); - assert_eq!(token_account.base.owner, payer.pubkey()); - - // mint via explicit recipient - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Mint.into(), - &token.to_string(), - "1", - &account.to_string(), - ], - ) - .await - .unwrap(); - amount += 1; - - let account_data = config.rpc_client.get_account(&account).await.unwrap(); - let token_account = - StateWithExtensionsOwned::::unpack(account_data.data).unwrap(); - assert_eq!(token_account.base.amount, amount); - assert_eq!(token_account.base.mint, token); - assert_eq!(token_account.base.owner, payer.pubkey()); - - // mint via explicit owner - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Mint.into(), - &token.to_string(), - "1", - "--recipient-owner", - &payer.pubkey().to_string(), - ], - ) - .await - .unwrap(); - amount += 1; - - let account_data = config.rpc_client.get_account(&account).await.unwrap(); - let token_account = - StateWithExtensionsOwned::::unpack(account_data.data).unwrap(); - assert_eq!(token_account.base.amount, amount); - assert_eq!(token_account.base.mint, token); - assert_eq!(token_account.base.owner, payer.pubkey()); - } - } - - #[tokio::test] - #[serial] - async fn balance_after_mint() { - let (test_validator, payer) = new_validator_for_test().await; - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(&test_validator, &payer, program_id); - let token = create_token(&config, &payer).await; - let account = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; - let ui_amount = 100.0; - mint_tokens(&config, &payer, token, ui_amount, account).await; - let result = process_test_command( - &config, - &payer, - &["spl-token", CommandName::Balance.into(), &token.to_string()], - ) - .await; - let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); - assert_eq!(value["amount"], format!("{}", ui_amount)); - assert_eq!(value["uiAmountString"], format!("{}", ui_amount)); - } - } - #[tokio::test] - #[serial] - async fn balance_after_mint_with_owner() { - let (test_validator, payer) = new_validator_for_test().await; - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(&test_validator, &payer, program_id); - let token = create_token(&config, &payer).await; - let account = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; - let ui_amount = 100.0; - mint_tokens(&config, &payer, token, ui_amount, account).await; - let config = test_config_without_default_signer(&test_validator, program_id); - let result = process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Balance.into(), - &token.to_string(), - "--owner", - &payer.pubkey().to_string(), - ], - ) - .await; - let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); - assert_eq!(value["amount"], format!("{}", ui_amount)); - assert_eq!(value["uiAmountString"], format!("{}", ui_amount)); - } - } - - #[tokio::test] - #[serial] - async fn accounts() { - let (test_validator, payer) = new_validator_for_test().await; - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(&test_validator, &payer, program_id); - let token1 = create_token(&config, &payer).await; - let _account1 = - create_associated_account(&config, &payer, &token1, &payer.pubkey()).await; - let token2 = create_token(&config, &payer).await; - let _account2 = - create_associated_account(&config, &payer, &token2, &payer.pubkey()).await; - let token3 = create_token(&config, &payer).await; - let result = process_test_command( - &config, - &payer, - &["spl-token", CommandName::Accounts.into()], - ) - .await - .unwrap(); - assert!(result.contains(&token1.to_string())); - assert!(result.contains(&token2.to_string())); - assert!(!result.contains(&token3.to_string())); - } - } - - #[tokio::test] - #[serial] - async fn accounts_with_owner() { - let (test_validator, payer) = new_validator_for_test().await; - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(&test_validator, &payer, program_id); - let token1 = create_token(&config, &payer).await; - let _account1 = - create_associated_account(&config, &payer, &token1, &payer.pubkey()).await; - let token2 = create_token(&config, &payer).await; - let _account2 = - create_associated_account(&config, &payer, &token2, &payer.pubkey()).await; - let token3 = create_token(&config, &payer).await; - let config = test_config_without_default_signer(&test_validator, program_id); - let result = process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Accounts.into(), - "--owner", - &payer.pubkey().to_string(), - ], - ) - .await - .unwrap(); - assert!(result.contains(&token1.to_string())); - assert!(result.contains(&token2.to_string())); - assert!(!result.contains(&token3.to_string())); - } - } - - #[tokio::test] - #[serial] - async fn wrap() { - let (test_validator, payer) = new_validator_for_test().await; - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(&test_validator, &payer, program_id); - let native_mint = *Token::new_native( - config.program_client.clone(), - program_id, - config.fee_payer().unwrap().clone(), - ) - .get_address(); - do_create_native_mint(&config, program_id, &payer).await; - let _result = process_test_command( - &config, - &payer, - &["spl-token", CommandName::Wrap.into(), "0.5"], - ) - .await - .unwrap(); - let account = get_associated_token_address_with_program_id( - &payer.pubkey(), - &native_mint, - &config.program_id, - ); - let account = config.rpc_client.get_account(&account).await.unwrap(); - let token_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - assert_eq!(token_account.base.mint, native_mint); - assert_eq!(token_account.base.owner, payer.pubkey()); - assert!(token_account.base.is_native()); - } - } - - #[tokio::test] - #[serial] - async fn unwrap() { - let (test_validator, payer) = new_validator_for_test().await; - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(&test_validator, &payer, program_id); - do_create_native_mint(&config, program_id, &payer).await; - let (signer, account) = new_throwaway_signer(); - let bulk_signers: Vec> = vec![Arc::new(clone_keypair(&payer)), signer]; - command_wrap( - &config, - 0.5, - payer.pubkey(), - Some(account), - false, - bulk_signers, - ) - .await - .unwrap(); - let result = process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Unwrap.into(), - &account.to_string(), - ], - ) - .await; - result.unwrap(); - config.rpc_client.get_account(&account).await.unwrap_err(); - } - } - - #[tokio::test] - #[serial] - async fn transfer() { - let (test_validator, payer) = new_validator_for_test().await; - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(&test_validator, &payer, program_id); - let token = create_token(&config, &payer).await; - let source = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; - let destination = create_auxiliary_account(&config, &payer, token).await; - let ui_amount = 100.0; - mint_tokens(&config, &payer, token, ui_amount, source).await; - let result = process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Transfer.into(), - &token.to_string(), - "10", - &destination.to_string(), - ], - ) - .await; - result.unwrap(); - - let account = config.rpc_client.get_account(&source).await.unwrap(); - let token_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - assert_eq!(token_account.base.amount, 90); - let account = config.rpc_client.get_account(&destination).await.unwrap(); - let token_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - assert_eq!(token_account.base.amount, 10); - } - } - - #[tokio::test] - #[serial] - async fn transfer_fund_recipient() { - let (test_validator, payer) = new_validator_for_test().await; - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(&test_validator, &payer, program_id); - let token = create_token(&config, &payer).await; - let source = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; - let recipient = Keypair::new().pubkey().to_string(); - let ui_amount = 100.0; - mint_tokens(&config, &payer, token, ui_amount, source).await; - let result = process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Transfer.into(), - "--fund-recipient", - "--allow-unfunded-recipient", - &token.to_string(), - "10", - &recipient, - ], - ) - .await; - result.unwrap(); - - let account = config.rpc_client.get_account(&source).await.unwrap(); - let token_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - assert_eq!(token_account.base.amount, 90); - } - } - - #[tokio::test] - #[serial] - async fn transfer_non_standard_recipient() { - let (test_validator, payer) = new_validator_for_test().await; - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - for other_program_id in VALID_TOKEN_PROGRAM_IDS - .iter() - .filter(|id| *id != program_id) - { - let mut config = - test_config_with_default_signer(&test_validator, &payer, other_program_id); - let wrong_program_token = create_token(&config, &payer).await; - let wrong_program_account = create_associated_account( - &config, - &payer, - &wrong_program_token, - &payer.pubkey(), - ) - .await; - config.program_id = *program_id; - let config = config; - - let token = create_token(&config, &payer).await; - let source = - create_associated_account(&config, &payer, &token, &payer.pubkey()).await; - let recipient = Keypair::new().pubkey(); - let recipient_token_account = get_associated_token_address_with_program_id( - &recipient, - &token, - &config.program_id, - ); - let system_token_account = get_associated_token_address_with_program_id( - &system_program::id(), - &token, - &config.program_id, - ); - let amount = 100; - mint_tokens(&config, &payer, token, amount as f64, source).await; - - // transfer fails to unfunded recipient without flag - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Transfer.into(), - "--fund-recipient", - &token.to_string(), - "1", - &recipient.to_string(), - ], - ) - .await - .unwrap_err(); - - // with unfunded flag, transfer goes through - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Transfer.into(), - "--fund-recipient", - "--allow-unfunded-recipient", - &token.to_string(), - "1", - &recipient.to_string(), - ], - ) - .await - .unwrap(); - let account = config - .rpc_client - .get_account(&recipient_token_account) - .await - .unwrap(); - let token_account = - StateWithExtensionsOwned::::unpack(account.data).unwrap(); - assert_eq!(token_account.base.amount, 1); - - // transfer fails to non-system recipient without flag - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Transfer.into(), - "--fund-recipient", - &token.to_string(), - "1", - &system_program::id().to_string(), - ], - ) - .await - .unwrap_err(); - - // with non-system flag, transfer goes through - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Transfer.into(), - "--fund-recipient", - "--allow-non-system-account-recipient", - &token.to_string(), - "1", - &system_program::id().to_string(), - ], - ) - .await - .unwrap(); - let account = config - .rpc_client - .get_account(&system_token_account) - .await - .unwrap(); - let token_account = - StateWithExtensionsOwned::::unpack(account.data).unwrap(); - assert_eq!(token_account.base.amount, 1); - - // transfer to same-program non-account fails - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Transfer.into(), - "--fund-recipient", - "--allow-non-system-account-recipient", - "--allow-unfunded-recipient", - &token.to_string(), - "1", - &token.to_string(), - ], - ) - .await - .unwrap_err(); - - // transfer to other-program account fails - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Transfer.into(), - "--fund-recipient", - "--allow-non-system-account-recipient", - "--allow-unfunded-recipient", - &token.to_string(), - "1", - &wrong_program_account.to_string(), - ], - ) - .await - .unwrap_err(); - } - } - } - - #[tokio::test] - #[serial] - async fn allow_non_system_account_recipient() { - let (test_validator, payer) = new_validator_for_test().await; - let config = test_config_with_default_signer(&test_validator, &payer, &spl_token::id()); - - let token = create_token(&config, &payer).await; - let source = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; - let recipient = Keypair::new().pubkey().to_string(); - let ui_amount = 100.0; - mint_tokens(&config, &payer, token, ui_amount, source).await; - let result = process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Transfer.into(), - "--fund-recipient", - "--allow-non-system-account-recipient", - "--allow-unfunded-recipient", - &token.to_string(), - "10", - &recipient, - ], - ) - .await; - result.unwrap(); - - let ui_account = config - .rpc_client - .get_token_account(&source) - .await - .unwrap() - .unwrap(); - assert_eq!(ui_account.token_amount.amount, "90"); - } - - #[tokio::test] - #[serial] - async fn close_account() { - let (test_validator, payer) = new_validator_for_test().await; - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(&test_validator, &payer, program_id); - - let native_mint = Token::new_native( - config.program_client.clone(), - program_id, - config.fee_payer().unwrap().clone(), - ); - do_create_native_mint(&config, program_id, &payer).await; - native_mint - .get_or_create_associated_account_info(&payer.pubkey()) - .await - .unwrap(); - - let token = create_token(&config, &payer).await; - - let system_recipient = Keypair::new().pubkey(); - let wsol_recipient = native_mint.get_associated_token_address(&payer.pubkey()); - - let token_rent_amount = config - .rpc_client - .get_account(&create_auxiliary_account(&config, &payer, token).await) - .await - .unwrap() - .lamports; - - for recipient in [system_recipient, wsol_recipient] { - let base_balance = config - .rpc_client - .get_account(&recipient) - .await - .map(|account| account.lamports) - .unwrap_or(0); - - let source = create_auxiliary_account(&config, &payer, token).await; - - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Close.into(), - "--address", - &source.to_string(), - "--recipient", - &recipient.to_string(), - ], - ) - .await - .unwrap(); - - let recipient_data = config.rpc_client.get_account(&recipient).await.unwrap(); - - assert_eq!(recipient_data.lamports, base_balance + token_rent_amount); - if recipient == wsol_recipient { - let recipient_account = - StateWithExtensionsOwned::::unpack(recipient_data.data).unwrap(); - assert_eq!(recipient_account.base.amount, token_rent_amount); - } - } - } - } - - #[tokio::test] - #[serial] - async fn close_wrapped_sol_account() { - let (test_validator, payer) = new_validator_for_test().await; - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(&test_validator, &payer, program_id); - let bulk_signers: Vec> = vec![Arc::new(clone_keypair(&payer))]; - - let native_mint = *Token::new_native( - config.program_client.clone(), - program_id, - config.fee_payer().unwrap().clone(), - ) - .get_address(); - let token = create_token(&config, &payer).await; - let source = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; - do_create_native_mint(&config, program_id, &payer).await; - let ui_amount = 10.0; - command_wrap( - &config, - ui_amount, - payer.pubkey(), - None, - false, - bulk_signers, - ) - .await - .unwrap(); - - let recipient = get_associated_token_address_with_program_id( - &payer.pubkey(), - &native_mint, - program_id, - ); - let result = process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Close.into(), - "--address", - &source.to_string(), - "--recipient", - &recipient.to_string(), - ], - ) - .await; - result.unwrap(); - - let ui_account = config - .rpc_client - .get_token_account(&recipient) - .await - .unwrap() - .unwrap(); - assert_eq!(ui_account.token_amount.amount, "10000000000"); - } - } - - #[tokio::test] - #[serial] - async fn disable_mint_authority() { - let (test_validator, payer) = new_validator_for_test().await; - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(&test_validator, &payer, program_id); - let token = create_token(&config, &payer).await; - let result = process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Authorize.into(), - &token.to_string(), - "mint", - "--disable", - ], - ) - .await; - result.unwrap(); - - let account = config.rpc_client.get_account(&token).await.unwrap(); - let mint = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - assert_eq!(mint.base.mint_authority, COption::None); - } - } - - #[tokio::test] - #[serial] - async fn gc() { - let (test_validator, payer) = new_validator_for_test().await; - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let mut config = test_config_with_default_signer(&test_validator, &payer, program_id); - let token = create_token(&config, &payer).await; - let _account = - create_associated_account(&config, &payer, &token, &payer.pubkey()).await; - let _aux1 = create_auxiliary_account(&config, &payer, token).await; - let _aux2 = create_auxiliary_account(&config, &payer, token).await; - let _aux3 = create_auxiliary_account(&config, &payer, token).await; - let result = process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Accounts.into(), - &token.to_string(), - ], - ) - .await - .unwrap(); - let value: serde_json::Value = serde_json::from_str(&result).unwrap(); - assert_eq!(value["accounts"].as_array().unwrap().len(), 4); - config.output_format = OutputFormat::Display; // fixup eventually? - let _result = - process_test_command(&config, &payer, &["spl-token", CommandName::Gc.into()]) - .await - .unwrap(); - config.output_format = OutputFormat::JsonCompact; - let result = process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Accounts.into(), - &token.to_string(), - ], - ) - .await - .unwrap(); - let value: serde_json::Value = serde_json::from_str(&result).unwrap(); - assert_eq!(value["accounts"].as_array().unwrap().len(), 1); - - config.output_format = OutputFormat::Display; - - // test implicit transfer - let token = create_token(&config, &payer).await; - let ata = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; - let aux = create_auxiliary_account(&config, &payer, token).await; - mint_tokens(&config, &payer, token, 1.0, ata).await; - mint_tokens(&config, &payer, token, 1.0, aux).await; - - process_test_command(&config, &payer, &["spl-token", CommandName::Gc.into()]) - .await - .unwrap(); - - let ui_ata = config - .rpc_client - .get_token_account(&ata) - .await - .unwrap() - .unwrap(); - - // aux is gone and its tokens are in ata - assert_eq!(ui_ata.token_amount.amount, "2"); - config.rpc_client.get_account(&aux).await.unwrap_err(); - - // test ata closure - let token = create_token(&config, &payer).await; - let ata = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; - - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Gc.into(), - "--close-empty-associated-accounts", - ], - ) - .await - .unwrap(); - - // ata is gone - config.rpc_client.get_account(&ata).await.unwrap_err(); - - // test a tricky corner case of both - let token = create_token(&config, &payer).await; - let ata = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; - let aux = create_auxiliary_account(&config, &payer, token).await; - mint_tokens(&config, &payer, token, 1.0, aux).await; - - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Gc.into(), - "--close-empty-associated-accounts", - ], - ) - .await - .unwrap(); - - let ui_ata = config - .rpc_client - .get_token_account(&ata) - .await - .unwrap() - .unwrap(); - - // aux is gone and its tokens are in ata, and ata has not been closed - assert_eq!(ui_ata.token_amount.amount, "1"); - config.rpc_client.get_account(&aux).await.unwrap_err(); - - // test that balance moves off an uncloseable account - let token = create_token(&config, &payer).await; - let ata = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; - let aux = create_auxiliary_account(&config, &payer, token).await; - let close_authority = Keypair::new().pubkey(); - mint_tokens(&config, &payer, token, 1.0, aux).await; - - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Authorize.into(), - &aux.to_string(), - "close", - &close_authority.to_string(), - ], - ) - .await - .unwrap(); - - process_test_command(&config, &payer, &["spl-token", CommandName::Gc.into()]) - .await - .unwrap(); - - let ui_ata = config - .rpc_client - .get_token_account(&ata) - .await - .unwrap() - .unwrap(); - - // aux tokens are now in ata - assert_eq!(ui_ata.token_amount.amount, "1"); - } - } - - #[tokio::test] - #[serial] - async fn set_owner() { - let (test_validator, payer) = new_validator_for_test().await; - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(&test_validator, &payer, program_id); - let token = create_token(&config, &payer).await; - let aux = create_auxiliary_account(&config, &payer, token).await; - let aux_string = aux.to_string(); - let _result = process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Authorize.into(), - &aux_string, - "owner", - &aux_string, - ], - ) - .await - .unwrap(); - let account = config.rpc_client.get_account(&aux).await.unwrap(); - let token_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - assert_eq!(token_account.base.mint, token); - assert_eq!(token_account.base.owner, aux); - } - } - - #[tokio::test] - #[serial] - async fn transfer_with_account_delegate() { - let (test_validator, payer) = new_validator_for_test().await; - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(&test_validator, &payer, program_id); - - let token = create_token(&config, &payer).await; - let source = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; - let destination = create_auxiliary_account(&config, &payer, token).await; - let delegate = Keypair::new(); - - let delegate_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(&delegate, &delegate_keypair_file).unwrap(); - let fee_payer_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(&payer, &fee_payer_keypair_file).unwrap(); - - let ui_amount = 100.0; - mint_tokens(&config, &payer, token, ui_amount, source).await; - - let ui_account = config - .rpc_client - .get_token_account(&source) - .await - .unwrap() - .unwrap(); - assert_eq!(ui_account.token_amount.amount, "100"); - assert_eq!(ui_account.delegate, None); - assert_eq!(ui_account.delegated_amount, None); - let ui_account = config - .rpc_client - .get_token_account(&destination) - .await - .unwrap() - .unwrap(); - assert_eq!(ui_account.token_amount.amount, "0"); - - exec_test_cmd( - &config, - &[ - "spl-token", - CommandName::Approve.into(), - &source.to_string(), - "10", - &delegate.pubkey().to_string(), - "--owner", - fee_payer_keypair_file.path().to_str().unwrap(), - "--fee-payer", - fee_payer_keypair_file.path().to_str().unwrap(), - "--program-id", - &program_id.to_string(), - ], - ) - .await - .unwrap(); - - let ui_account = config - .rpc_client - .get_token_account(&source) - .await - .unwrap() - .unwrap(); - assert_eq!(ui_account.delegate.unwrap(), delegate.pubkey().to_string()); - assert_eq!(ui_account.delegated_amount.unwrap().amount, "10"); - - let result = exec_test_cmd( - &config, - &[ - "spl-token", - CommandName::Transfer.into(), - &token.to_string(), - "10", - &destination.to_string(), - "--from", - &source.to_string(), - "--owner", - delegate_keypair_file.path().to_str().unwrap(), - "--fee-payer", - fee_payer_keypair_file.path().to_str().unwrap(), - "--program-id", - &program_id.to_string(), - ], - ) - .await; - result.unwrap(); - - let ui_account = config - .rpc_client - .get_token_account(&source) - .await - .unwrap() - .unwrap(); - assert_eq!(ui_account.token_amount.amount, "90"); - assert_eq!(ui_account.delegate, None); - assert_eq!(ui_account.delegated_amount, None); - let ui_account = config - .rpc_client - .get_token_account(&destination) - .await - .unwrap() - .unwrap(); - assert_eq!(ui_account.token_amount.amount, "10"); - } - } - - #[tokio::test] - #[serial] - async fn burn_with_account_delegate() { - let (test_validator, payer) = new_validator_for_test().await; - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(&test_validator, &payer, program_id); - - let token = create_token(&config, &payer).await; - let source = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; - let delegate = Keypair::new(); - - let delegate_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(&delegate, &delegate_keypair_file).unwrap(); - let fee_payer_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(&payer, &fee_payer_keypair_file).unwrap(); - - let ui_amount = 100.0; - mint_tokens(&config, &payer, token, ui_amount, source).await; - - let ui_account = config - .rpc_client - .get_token_account(&source) - .await - .unwrap() - .unwrap(); - assert_eq!(ui_account.token_amount.amount, "100"); - assert_eq!(ui_account.delegate, None); - assert_eq!(ui_account.delegated_amount, None); - - exec_test_cmd( - &config, - &[ - "spl-token", - CommandName::Approve.into(), - &source.to_string(), - "10", - &delegate.pubkey().to_string(), - "--owner", - fee_payer_keypair_file.path().to_str().unwrap(), - "--fee-payer", - fee_payer_keypair_file.path().to_str().unwrap(), - "--program-id", - &program_id.to_string(), - ], - ) - .await - .unwrap(); - - let ui_account = config - .rpc_client - .get_token_account(&source) - .await - .unwrap() - .unwrap(); - assert_eq!(ui_account.delegate.unwrap(), delegate.pubkey().to_string()); - assert_eq!(ui_account.delegated_amount.unwrap().amount, "10"); - - let result = exec_test_cmd( - &config, - &[ - "spl-token", - CommandName::Burn.into(), - &source.to_string(), - "10", - "--owner", - delegate_keypair_file.path().to_str().unwrap(), - "--fee-payer", - fee_payer_keypair_file.path().to_str().unwrap(), - "--program-id", - &program_id.to_string(), - ], - ) - .await; - result.unwrap(); - - let ui_account = config - .rpc_client - .get_token_account(&source) - .await - .unwrap() - .unwrap(); - assert_eq!(ui_account.token_amount.amount, "90"); - assert_eq!(ui_account.delegate, None); - assert_eq!(ui_account.delegated_amount, None); - } - } - - #[tokio::test] - #[serial] - async fn close_mint() { - let (test_validator, payer) = new_validator_for_test().await; - let config = - test_config_with_default_signer(&test_validator, &payer, &spl_token_2022::id()); - - let token_keypair = Keypair::new(); - let token_pubkey = token_keypair.pubkey(); - let bulk_signers: Vec> = - vec![Arc::new(clone_keypair(&payer)), Arc::new(token_keypair)]; - - command_create_token( - &config, - TEST_DECIMALS, - token_pubkey, - payer.pubkey(), - false, - true, - false, - false, - None, - None, - None, - None, - None, - None, - None, - false, - bulk_signers, - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&token_pubkey).await.unwrap(); - let test_mint = StateWithExtensionsOwned::::unpack(account.data); - assert!(test_mint.is_ok()); - - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::CloseMint.into(), - &token_pubkey.to_string(), - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&token_pubkey).await; - assert!(account.is_err()); - } - - #[tokio::test] - #[serial] - async fn burn_with_permanent_delegate() { - let (test_validator, payer) = new_validator_for_test().await; - let config = - test_config_with_default_signer(&test_validator, &payer, &spl_token_2022::id()); - - let token_keypair = Keypair::new(); - let token = token_keypair.pubkey(); - let bulk_signers: Vec> = - vec![Arc::new(clone_keypair(&payer)), Arc::new(token_keypair)]; - - command_create_token( - &config, - TEST_DECIMALS, - token, - payer.pubkey(), - false, - false, - false, - true, - None, - None, - None, - None, - None, - None, - None, - false, - bulk_signers, - ) - .await - .unwrap(); - - let permanent_delegate_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(&payer, &permanent_delegate_keypair_file).unwrap(); - - let unknown_owner = Keypair::new(); - let source = - create_associated_account(&config, &unknown_owner, &token, &unknown_owner.pubkey()) - .await; - let ui_amount = 100.0; - - mint_tokens(&config, &payer, token, ui_amount, source).await; - - let ui_account = config - .rpc_client - .get_token_account(&source) - .await - .unwrap() - .unwrap(); - - assert_eq!(ui_account.token_amount.amount, "100"); - - exec_test_cmd( - &config, - &[ - "spl-token", - CommandName::Burn.into(), - &source.to_string(), - "10", - "--owner", - permanent_delegate_keypair_file.path().to_str().unwrap(), - ], - ) - .await - .unwrap(); - - let ui_account = config - .rpc_client - .get_token_account(&source) - .await - .unwrap() - .unwrap(); - - assert_eq!(ui_account.token_amount.amount, "90"); - } - - #[tokio::test] - #[serial] - async fn transfer_with_permanent_delegate() { - let (test_validator, payer) = new_validator_for_test().await; - let config = - test_config_with_default_signer(&test_validator, &payer, &spl_token_2022::id()); - - let token_keypair = Keypair::new(); - let token = token_keypair.pubkey(); - let bulk_signers: Vec> = - vec![Arc::new(clone_keypair(&payer)), Arc::new(token_keypair)]; - - command_create_token( - &config, - TEST_DECIMALS, - token, - payer.pubkey(), - false, - false, - false, - true, - None, - None, - None, - None, - None, - None, - None, - false, - bulk_signers, - ) - .await - .unwrap(); - - let unknown_owner = Keypair::new(); - let source = - create_associated_account(&config, &unknown_owner, &token, &unknown_owner.pubkey()) - .await; - let destination = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; - - let permanent_delegate_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(&payer, &permanent_delegate_keypair_file).unwrap(); - - let ui_amount = 100.0; - mint_tokens(&config, &payer, token, ui_amount, source).await; - - let ui_account = config - .rpc_client - .get_token_account(&source) - .await - .unwrap() - .unwrap(); - - assert_eq!(ui_account.token_amount.amount, "100"); - - let ui_account = config - .rpc_client - .get_token_account(&destination) - .await - .unwrap() - .unwrap(); - - assert_eq!(ui_account.token_amount.amount, "0"); - - exec_test_cmd( - &config, - &[ - "spl-token", - CommandName::Transfer.into(), - &token.to_string(), - "50", - &destination.to_string(), - "--from", - &source.to_string(), - "--owner", - permanent_delegate_keypair_file.path().to_str().unwrap(), - ], - ) - .await - .unwrap(); - - let ui_account = config - .rpc_client - .get_token_account(&destination) - .await - .unwrap() - .unwrap(); - - assert_eq!(ui_account.token_amount.amount, "50"); - - let ui_account = config - .rpc_client - .get_token_account(&source) - .await - .unwrap() - .unwrap(); - - assert_eq!(ui_account.token_amount.amount, "50"); - } - - #[tokio::test] - #[serial] - async fn required_transfer_memos() { - let (test_validator, payer) = new_validator_for_test().await; - let program_id = spl_token_2022::id(); - let config = test_config_with_default_signer(&test_validator, &payer, &program_id); - let token = create_token(&config, &payer).await; - let token_account = - create_associated_account(&config, &payer, &token, &payer.pubkey()).await; - let source_account = create_auxiliary_account(&config, &payer, token).await; - mint_tokens(&config, &payer, token, 100.0, source_account).await; - - // enable works - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::EnableRequiredTransferMemos.into(), - &token_account.to_string(), - ], - ) - .await - .unwrap(); - let extensions = StateWithExtensionsOwned::::unpack( - config - .rpc_client - .get_account(&token_account) - .await - .unwrap() - .data, - ) - .unwrap(); - let memo_transfer = extensions.get_extension::().unwrap(); - let enabled: bool = memo_transfer.require_incoming_transfer_memos.into(); - assert!(enabled); - - // transfer requires a memo - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Transfer.into(), - "--from", - &source_account.to_string(), - &token.to_string(), - "1", - &token_account.to_string(), - ], - ) - .await - .unwrap_err(); - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Transfer.into(), - "--from", - &source_account.to_string(), - // malicious compliance - "--with-memo", - "memo", - &token.to_string(), - "1", - &token_account.to_string(), - ], - ) - .await - .unwrap(); - let account_data = config.rpc_client.get_account(&token_account).await.unwrap(); - let account_state = StateWithExtensionsOwned::::unpack(account_data.data).unwrap(); - assert_eq!(account_state.base.amount, 1); - - // disable works - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::DisableRequiredTransferMemos.into(), - &token_account.to_string(), - ], - ) - .await - .unwrap(); - let extensions = StateWithExtensionsOwned::::unpack( - config - .rpc_client - .get_account(&token_account) - .await - .unwrap() - .data, - ) - .unwrap(); - let memo_transfer = extensions.get_extension::().unwrap(); - let enabled: bool = memo_transfer.require_incoming_transfer_memos.into(); - assert!(!enabled); - } - - #[tokio::test] - #[serial] - async fn cpi_guard() { - let (test_validator, payer) = new_validator_for_test().await; - let program_id = spl_token_2022::id(); - let config = test_config_with_default_signer(&test_validator, &payer, &program_id); - let token = create_token(&config, &payer).await; - let token_account = - create_associated_account(&config, &payer, &token, &payer.pubkey()).await; - - // enable works - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::EnableCpiGuard.into(), - &token_account.to_string(), - ], - ) - .await - .unwrap(); - let extensions = StateWithExtensionsOwned::::unpack( - config - .rpc_client - .get_account(&token_account) - .await - .unwrap() - .data, - ) - .unwrap(); - let cpi_guard = extensions.get_extension::().unwrap(); - let enabled: bool = cpi_guard.lock_cpi.into(); - assert!(enabled); - - // disable works - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::DisableCpiGuard.into(), - &token_account.to_string(), - ], - ) - .await - .unwrap(); - let extensions = StateWithExtensionsOwned::::unpack( - config - .rpc_client - .get_account(&token_account) - .await - .unwrap() - .data, - ) - .unwrap(); - let cpi_guard = extensions.get_extension::().unwrap(); - let enabled: bool = cpi_guard.lock_cpi.into(); - assert!(!enabled); - } - - #[tokio::test] - #[serial] - async fn immutable_accounts() { - let (test_validator, payer) = new_validator_for_test().await; - let program_id = spl_token_2022::id(); - let config = test_config_with_default_signer(&test_validator, &payer, &program_id); - let token = create_token(&config, &payer).await; - let new_owner = Keypair::new().pubkey(); - let bulk_signers: Vec> = vec![Arc::new(clone_keypair(&payer))]; - let native_mint = *Token::new_native( - config.program_client.clone(), - &program_id, - config.fee_payer().unwrap().clone(), - ) - .get_address(); - do_create_native_mint(&config, &program_id, &payer).await; - - // cannot reassign an ata - let account = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; - let result = command_authorize( - &config, - account, - CliAuthorityType::Owner, - payer.pubkey(), - Some(new_owner), - true, - bulk_signers.clone(), - ) - .await; - result.unwrap_err(); - - // immutable works for create-account - let aux_account = Keypair::new(); - let aux_pubkey = aux_account.pubkey(); - let aux_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(&aux_account, &aux_keypair_file).unwrap(); - - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::CreateAccount.into(), - &token.to_string(), - aux_keypair_file.path().to_str().unwrap(), - "--immutable", - ], - ) - .await - .unwrap(); - - let result = command_authorize( - &config, - aux_pubkey, - CliAuthorityType::Owner, - payer.pubkey(), - Some(new_owner), - true, - bulk_signers.clone(), - ) - .await; - result.unwrap_err(); - - // immutable works for wrap - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Wrap.into(), - "--create-aux-account", - "--immutable", - "0.5", - ], - ) - .await - .unwrap(); - - let accounts = config - .rpc_client - .get_token_accounts_by_owner(&payer.pubkey(), TokenAccountsFilter::Mint(native_mint)) - .await - .unwrap(); - - let result = command_authorize( - &config, - Pubkey::from_str(&accounts[0].pubkey).unwrap(), - CliAuthorityType::Owner, - payer.pubkey(), - Some(new_owner), - true, - bulk_signers.clone(), - ) - .await; - result.unwrap_err(); - } - - #[tokio::test] - #[serial] - async fn non_transferable() { - let (test_validator, payer) = new_validator_for_test().await; - let config = - test_config_with_default_signer(&test_validator, &payer, &spl_token_2022::id()); - - let token_keypair = Keypair::new(); - let token_pubkey = token_keypair.pubkey(); - let bulk_signers: Vec> = - vec![Arc::new(clone_keypair(&payer)), Arc::new(token_keypair)]; - - command_create_token( - &config, - TEST_DECIMALS, - token_pubkey, - payer.pubkey(), - false, - false, - true, - false, - None, - None, - None, - None, - None, - None, - None, - false, - bulk_signers, - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&token_pubkey).await.unwrap(); - let test_mint = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - assert!(test_mint.get_extension::().is_ok()); - - let associated_account = - create_associated_account(&config, &payer, &token_pubkey, &payer.pubkey()).await; - let aux_account = create_auxiliary_account(&config, &payer, token_pubkey).await; - mint_tokens(&config, &payer, token_pubkey, 100.0, associated_account).await; - - // transfer not allowed - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Transfer.into(), - "--from", - &associated_account.to_string(), - &token_pubkey.to_string(), - "1", - &aux_account.to_string(), - ], - ) - .await - .unwrap_err(); - } - - #[tokio::test] - #[serial] - async fn default_account_state() { - let (test_validator, payer) = new_validator_for_test().await; - let program_id = spl_token_2022::id(); - let config = test_config_with_default_signer(&test_validator, &payer, &program_id); - let token_keypair = Keypair::new(); - let token_pubkey = token_keypair.pubkey(); - let bulk_signers: Vec> = - vec![Arc::new(clone_keypair(&payer)), Arc::new(token_keypair)]; - - command_create_token( - &config, - TEST_DECIMALS, - token_pubkey, - payer.pubkey(), - true, - false, - false, - false, - None, - None, - None, - Some(AccountState::Frozen), - None, - None, - None, - false, - bulk_signers, - ) - .await - .unwrap(); - - let mint_account = config.rpc_client.get_account(&token_pubkey).await.unwrap(); - let mint = StateWithExtensionsOwned::::unpack(mint_account.data).unwrap(); - let extension = mint.get_extension::().unwrap(); - assert_eq!(extension.state, u8::from(AccountState::Frozen)); - - let frozen_account = - create_associated_account(&config, &payer, &token_pubkey, &payer.pubkey()).await; - let token_account = config - .rpc_client - .get_account(&frozen_account) - .await - .unwrap(); - let account = StateWithExtensionsOwned::::unpack(token_account.data).unwrap(); - assert_eq!(account.base.state, AccountState::Frozen); - - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::UpdateDefaultAccountState.into(), - &token_pubkey.to_string(), - "initialized", - ], - ) - .await - .unwrap(); - let unfrozen_account = create_auxiliary_account(&config, &payer, token_pubkey).await; - let token_account = config - .rpc_client - .get_account(&unfrozen_account) - .await - .unwrap(); - let account = StateWithExtensionsOwned::::unpack(token_account.data).unwrap(); - assert_eq!(account.base.state, AccountState::Initialized); - } - - #[tokio::test] - #[serial] - async fn transfer_fee() { - let (test_validator, payer) = new_validator_for_test().await; - let config = - test_config_with_default_signer(&test_validator, &payer, &spl_token_2022::id()); - - let token_keypair = Keypair::new(); - let token_pubkey = token_keypair.pubkey(); - let bulk_signers: Vec> = - vec![Arc::new(clone_keypair(&payer)), Arc::new(token_keypair)]; - let transfer_fee_basis_points = 100; - let maximum_fee = 2_000_000; - - command_create_token( - &config, - TEST_DECIMALS, - token_pubkey, - payer.pubkey(), - false, - false, - false, - false, - None, - None, - None, - None, - Some((transfer_fee_basis_points, maximum_fee)), - None, - None, - false, - bulk_signers, - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&token_pubkey).await.unwrap(); - let test_mint = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = test_mint.get_extension::().unwrap(); - assert_eq!( - u16::from(extension.older_transfer_fee.transfer_fee_basis_points), - transfer_fee_basis_points - ); - assert_eq!( - u64::from(extension.older_transfer_fee.maximum_fee), - maximum_fee - ); - assert_eq!( - u16::from(extension.newer_transfer_fee.transfer_fee_basis_points), - transfer_fee_basis_points - ); - assert_eq!( - u64::from(extension.newer_transfer_fee.maximum_fee), - maximum_fee - ); - - let total_amount = 1000.0; - let transfer_amount = 100.0; - let token_account = - create_associated_account(&config, &payer, &token_pubkey, &payer.pubkey()).await; - let source_account = create_auxiliary_account(&config, &payer, token_pubkey).await; - mint_tokens(&config, &payer, token_pubkey, total_amount, source_account).await; - - // withdraw from account directly - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Transfer.into(), - "--from", - &source_account.to_string(), - &token_pubkey.to_string(), - &transfer_amount.to_string(), - &token_account.to_string(), - "--expected-fee", - "1", - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&token_account).await.unwrap(); - let account_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = account_state.get_extension::().unwrap(); - let withheld_amount = - spl_token::amount_to_ui_amount(u64::from(extension.withheld_amount), TEST_DECIMALS); - assert_eq!(withheld_amount, 1.0); - - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::WithdrawWithheldTokens.into(), - &token_account.to_string(), - &token_account.to_string(), - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&token_account).await.unwrap(); - let account_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = account_state.get_extension::().unwrap(); - assert_eq!(u64::from(extension.withheld_amount), 0); - - // withdraw from mint after account closure - // gather fees - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Transfer.into(), - "--from", - &source_account.to_string(), - &token_pubkey.to_string(), - &(total_amount - transfer_amount).to_string(), - &token_account.to_string(), - "--expected-fee", - "9", - ], - ) - .await - .unwrap(); - - // burn tokens - let account = config.rpc_client.get_account(&token_account).await.unwrap(); - let account_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let burn_amount = spl_token::amount_to_ui_amount(account_state.base.amount, TEST_DECIMALS); - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Burn.into(), - &token_account.to_string(), - &burn_amount.to_string(), - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&token_account).await.unwrap(); - let account_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = account_state.get_extension::().unwrap(); - let withheld_amount = - spl_token::amount_to_ui_amount(u64::from(extension.withheld_amount), TEST_DECIMALS); - assert_eq!(withheld_amount, 9.0); - - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Close.into(), - "--address", - &token_account.to_string(), - "--recipient", - &payer.pubkey().to_string(), - ], - ) - .await - .unwrap(); - - let mint = config.rpc_client.get_account(&token_pubkey).await.unwrap(); - let mint_state = StateWithExtensionsOwned::::unpack(mint.data).unwrap(); - let extension = mint_state.get_extension::().unwrap(); - let withheld_amount = - spl_token::amount_to_ui_amount(u64::from(extension.withheld_amount), TEST_DECIMALS); - assert_eq!(withheld_amount, 9.0); - - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::WithdrawWithheldTokens.into(), - &source_account.to_string(), - "--include-mint", - ], - ) - .await - .unwrap(); - - let mint = config.rpc_client.get_account(&token_pubkey).await.unwrap(); - let mint_state = StateWithExtensionsOwned::::unpack(mint.data).unwrap(); - let extension = mint_state.get_extension::().unwrap(); - assert_eq!(u64::from(extension.withheld_amount), 0); - - // set the transfer fee - let new_transfer_fee_basis_points = 800; - let new_maximum_fee = 5_000_000.0; - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::SetTransferFee.into(), - &token_pubkey.to_string(), - &new_transfer_fee_basis_points.to_string(), - &new_maximum_fee.to_string(), - ], - ) - .await - .unwrap(); - - let mint = config.rpc_client.get_account(&token_pubkey).await.unwrap(); - let mint_state = StateWithExtensionsOwned::::unpack(mint.data).unwrap(); - let extension = mint_state.get_extension::().unwrap(); - assert_eq!( - u16::from(extension.newer_transfer_fee.transfer_fee_basis_points), - new_transfer_fee_basis_points - ); - let new_maximum_fee = spl_token::ui_amount_to_amount(new_maximum_fee, TEST_DECIMALS); - assert_eq!( - u64::from(extension.newer_transfer_fee.maximum_fee), - new_maximum_fee - ); - - // disable transfer fee authority - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Authorize.into(), - "--disable", - &token_pubkey.to_string(), - "transfer-fee-config", - ], - ) - .await - .unwrap(); - - let mint = config.rpc_client.get_account(&token_pubkey).await.unwrap(); - let mint_state = StateWithExtensionsOwned::::unpack(mint.data).unwrap(); - let extension = mint_state.get_extension::().unwrap(); - - assert_eq!( - Option::::try_from(extension.transfer_fee_config_authority).unwrap(), - None, - ); - - // disable withdraw withheld authority - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Authorize.into(), - "--disable", - &token_pubkey.to_string(), - "withheld-withdraw", - ], - ) - .await - .unwrap(); - - let mint = config.rpc_client.get_account(&token_pubkey).await.unwrap(); - let mint_state = StateWithExtensionsOwned::::unpack(mint.data).unwrap(); - let extension = mint_state.get_extension::().unwrap(); - - assert_eq!( - Option::::try_from(extension.withdraw_withheld_authority).unwrap(), - None, - ); - } - - #[tokio::test] - #[serial] - async fn confidential_transfer() { - use spl_token_2022::solana_zk_token_sdk::encryption::elgamal::ElGamalKeypair; - - let (test_validator, payer) = new_validator_for_test().await; - let config = - test_config_with_default_signer(&test_validator, &payer, &spl_token_2022::id()); - - // create token with confidential transfers enabled - let token_keypair = Keypair::new(); - let token_pubkey = token_keypair.pubkey(); - let bulk_signers: Vec> = - vec![Arc::new(clone_keypair(&payer)), Arc::new(token_keypair)]; - let confidential_transfer_mint_authority = payer.pubkey(); - let auto_approve = false; - - command_create_token( - &config, - TEST_DECIMALS, - token_pubkey, - payer.pubkey(), - false, - false, - false, - false, - None, - None, - None, - None, - None, - Some(auto_approve), - None, - false, - bulk_signers.clone(), - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&token_pubkey).await.unwrap(); - let test_mint = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = test_mint - .get_extension::() - .unwrap(); - - assert_eq!( - Option::::from(extension.authority), - Some(confidential_transfer_mint_authority), - ); - assert_eq!( - bool::from(extension.auto_approve_new_accounts), - auto_approve, - ); - assert_eq!( - Option::::from(extension.auditor_elgamal_pubkey), - None, - ); - - // update confidential transfer mint settings - let auditor_keypair = ElGamalKeypair::new_rand(); - let auditor_pubkey: ElGamalPubkey = (*auditor_keypair.pubkey()).into(); - let new_auto_approve = true; - - command_update_confidential_transfer_settings( - &config, - token_pubkey, - confidential_transfer_mint_authority, - Some(new_auto_approve), - Some(ElGamalPubkeyOrNone::ElGamalPubkey(auditor_pubkey)), // auditor pubkey - bulk_signers.clone(), - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&token_pubkey).await.unwrap(); - let test_mint = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = test_mint - .get_extension::() - .unwrap(); - - assert_eq!( - bool::from(extension.auto_approve_new_accounts), - new_auto_approve, - ); - assert_eq!( - Option::::from(extension.auditor_elgamal_pubkey), - Some(auditor_pubkey), - ); - - // create a confidential transfer account - let token_account = - create_associated_account(&config, &payer, &token_pubkey, &payer.pubkey()).await; - - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::ConfigureConfidentialTransferAccount.into(), - &token_pubkey.to_string(), - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&token_account).await.unwrap(); - let account_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = account_state - .get_extension::() - .unwrap(); - assert!(bool::from(extension.approved)); - assert!(bool::from(extension.allow_confidential_credits)); - assert!(bool::from(extension.allow_non_confidential_credits)); - - // disable and enable confidential transfers for an account - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::DisableConfidentialCredits.into(), - &token_pubkey.to_string(), - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&token_account).await.unwrap(); - let account_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = account_state - .get_extension::() - .unwrap(); - assert!(!bool::from(extension.allow_confidential_credits)); - - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::EnableConfidentialCredits.into(), - &token_pubkey.to_string(), - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&token_account).await.unwrap(); - let account_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = account_state - .get_extension::() - .unwrap(); - assert!(bool::from(extension.allow_confidential_credits)); - - // disable and eanble non-confidential transfers for an account - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::DisableNonConfidentialCredits.into(), - &token_pubkey.to_string(), - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&token_account).await.unwrap(); - let account_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = account_state - .get_extension::() - .unwrap(); - assert!(!bool::from(extension.allow_non_confidential_credits)); - - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::EnableNonConfidentialCredits.into(), - &token_pubkey.to_string(), - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&token_account).await.unwrap(); - let account_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = account_state - .get_extension::() - .unwrap(); - assert!(bool::from(extension.allow_non_confidential_credits)); - - // deposit confidential tokens - let deposit_amount = 100.0; - mint_tokens(&config, &payer, token_pubkey, deposit_amount, token_account).await; - - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::DepositConfidentialTokens.into(), - &token_pubkey.to_string(), - &deposit_amount.to_string(), - ], - ) - .await - .unwrap(); - - // apply pending balance - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::ApplyPendingBalance.into(), - &token_pubkey.to_string(), - ], - ) - .await - .unwrap(); - - // confidential transfer - let destination_account = create_auxiliary_account(&config, &payer, token_pubkey).await; - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::ConfigureConfidentialTransferAccount.into(), - "--address", - &destination_account.to_string(), - ], - ) - .await - .unwrap(); // configure destination account for confidential transfers first - - let transfer_amount = 100.0; - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Transfer.into(), - &token_pubkey.to_string(), - &transfer_amount.to_string(), - &destination_account.to_string(), - "--confidential", - ], - ) - .await - .unwrap(); - - // withdraw confidential tokens - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::ApplyPendingBalance.into(), - "--address", - &destination_account.to_string(), - ], - ) - .await - .unwrap(); // apply pending balance first - - let withdraw_amount = 100.0; - - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::WithdrawConfidentialTokens.into(), - &token_pubkey.to_string(), - &withdraw_amount.to_string(), - "--address", - &destination_account.to_string(), - ], - ) - .await - .unwrap(); - - // disable confidential transfers for mint - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Authorize.into(), - &token_pubkey.to_string(), - "confidential-transfer-mint", - "--disable", - ], - ) - .await - .unwrap(); - } - - #[tokio::test] - #[serial] - async fn multisig_transfer() { - let (test_validator, payer) = new_validator_for_test().await; - let m = 3; - let n = 5u8; - // need to add "payer" to make the config provide the right signer - let (multisig_members, multisig_paths): (Vec<_>, Vec<_>) = - std::iter::once(clone_keypair(&payer)) - .chain(std::iter::repeat_with(Keypair::new).take((n - 2) as usize)) - .map(|s| { - let keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(&s, &keypair_file).unwrap(); - (s.pubkey(), keypair_file) - }) - .unzip(); - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(&test_validator, &payer, program_id); - let token = create_token(&config, &payer).await; - let multisig = Arc::new(Keypair::new()); - let multisig_pubkey = multisig.pubkey(); - - // add the multisig as a member to itself, make it self-owned - let multisig_members = std::iter::once(multisig_pubkey) - .chain(multisig_members.iter().cloned()) - .collect::>(); - let multisig_path = NamedTempFile::new().unwrap(); - write_keypair_file(&multisig, &multisig_path).unwrap(); - let multisig_paths = std::iter::once(&multisig_path) - .chain(multisig_paths.iter()) - .collect::>(); - - command_create_multisig(&config, multisig, m, multisig_members) - .await - .unwrap(); - - let account = config - .rpc_client - .get_account(&multisig_pubkey) - .await - .unwrap(); - let multisig = Multisig::unpack(&account.data).unwrap(); - assert_eq!(multisig.m, m); - assert_eq!(multisig.n, n); - - let source = create_associated_account(&config, &payer, &token, &multisig_pubkey).await; - let destination = create_auxiliary_account(&config, &payer, token).await; - let ui_amount = 100.0; - mint_tokens(&config, &payer, token, ui_amount, source).await; - - exec_test_cmd( - &config, - &[ - "spl-token", - CommandName::Transfer.into(), - &token.to_string(), - "10", - &destination.to_string(), - "--multisig-signer", - multisig_paths[0].path().to_str().unwrap(), - "--multisig-signer", - multisig_paths[1].path().to_str().unwrap(), - "--multisig-signer", - multisig_paths[2].path().to_str().unwrap(), - "--from", - &source.to_string(), - "--owner", - &multisig_pubkey.to_string(), - "--fee-payer", - multisig_paths[1].path().to_str().unwrap(), - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&source).await.unwrap(); - let token_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - assert_eq!(token_account.base.amount, 90); - let account = config.rpc_client.get_account(&destination).await.unwrap(); - let token_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - assert_eq!(token_account.base.amount, 10); - } - } - - #[tokio::test] - #[serial] - async fn offline_multisig_transfer_with_nonce() { - let (test_validator, payer) = new_validator_for_test().await; - let m = 2; - let n = 3u8; - - let (multisig_members, multisig_paths): (Vec<_>, Vec<_>) = - std::iter::repeat_with(Keypair::new) - .take(n as usize) - .map(|s| { - let keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(&s, &keypair_file).unwrap(); - (s.pubkey(), keypair_file) - }) - .unzip(); - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let mut config = test_config_with_default_signer(&test_validator, &payer, program_id); - let token = create_token(&config, &payer).await; - let nonce = create_nonce(&config, &payer).await; - - let nonce_account = config.rpc_client.get_account(&nonce).await.unwrap(); - let start_hash_index = 4 + 4 + 32; - let blockhash = Hash::new(&nonce_account.data[start_hash_index..start_hash_index + 32]); - - let multisig = Arc::new(Keypair::new()); - let multisig_pubkey = multisig.pubkey(); - - command_create_multisig(&config, multisig, m, multisig_members.clone()) - .await - .unwrap(); - - let source = create_associated_account(&config, &payer, &token, &multisig_pubkey).await; - let destination = create_auxiliary_account(&config, &payer, token).await; - let ui_amount = 100.0; - mint_tokens(&config, &payer, token, ui_amount, source).await; - - let program_client: Arc> = Arc::new( - ProgramOfflineClient::new(blockhash, ProgramRpcClientSendTransaction), - ); - config.program_client = program_client; - let result = exec_test_cmd( - &config, - &[ - "spl-token", - CommandName::Transfer.into(), - &token.to_string(), - "10", - &destination.to_string(), - "--blockhash", - &blockhash.to_string(), - "--nonce", - &nonce.to_string(), - "--nonce-authority", - &payer.pubkey().to_string(), - "--sign-only", - "--mint-decimals", - &format!("{}", TEST_DECIMALS), - "--multisig-signer", - multisig_paths[1].path().to_str().unwrap(), - "--multisig-signer", - &multisig_members[2].to_string(), - "--from", - &source.to_string(), - "--owner", - &multisig_pubkey.to_string(), - "--fee-payer", - &multisig_members[0].to_string(), - ], - ) - .await - .unwrap(); - // the provided signer has a signature, denoted by the pubkey followed - // by "=" and the signature - assert!(result.contains(&format!("{}=", multisig_members[1]))); - - // other three expected signers are absent - let absent_signers_position = result.find("Absent Signers").unwrap(); - let absent_signers = result.get(absent_signers_position..).unwrap(); - assert!(absent_signers.contains(&multisig_members[0].to_string())); - assert!(absent_signers.contains(&multisig_members[2].to_string())); - assert!(absent_signers.contains(&payer.pubkey().to_string())); - - // and nothing else is marked a signer - assert!(!absent_signers.contains(&multisig_pubkey.to_string())); - assert!(!absent_signers.contains(&nonce.to_string())); - assert!(!absent_signers.contains(&source.to_string())); - assert!(!absent_signers.contains(&destination.to_string())); - assert!(!absent_signers.contains(&token.to_string())); - } - } - - #[tokio::test] - #[serial] - async fn withdraw_excess_lamports_from_multisig() { - let (test_validator, payer) = new_validator_for_test().await; - let m = 3; - let n = 5u8; - // need to add "payer" to make the config provide the right signer - let (multisig_members, multisig_paths): (Vec<_>, Vec<_>) = - std::iter::once(clone_keypair(&payer)) - .chain(std::iter::repeat_with(Keypair::new).take((n - 2) as usize)) - .map(|s| { - let keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(&s, &keypair_file).unwrap(); - (s.pubkey(), keypair_file) - }) - .unzip(); - - let fee_payer_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(&payer, &fee_payer_keypair_file).unwrap(); - - let owner_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(&payer, &owner_keypair_file).unwrap(); - - let program_id = &spl_token_2022::id(); - let config = test_config_with_default_signer(&test_validator, &payer, program_id); - - let multisig = Arc::new(Keypair::new()); - let multisig_pubkey = multisig.pubkey(); - - // add the multisig as a member to itself, make it self-owned - let multisig_members = std::iter::once(multisig_pubkey) - .chain(multisig_members.iter().cloned()) - .collect::>(); - let multisig_path = NamedTempFile::new().unwrap(); - write_keypair_file(&multisig, &multisig_path).unwrap(); - let multisig_paths = std::iter::once(&multisig_path) - .chain(multisig_paths.iter()) - .collect::>(); - - command_create_multisig(&config, multisig, m, multisig_members) - .await - .unwrap(); - - let account = config - .rpc_client - .get_account(&multisig_pubkey) - .await - .unwrap(); - let multisig = Multisig::unpack(&account.data).unwrap(); - assert_eq!(multisig.m, m); - assert_eq!(multisig.n, n); - - let receiver = Keypair::new(); - let excess_lamports = 4000 * 1_000_000_000; - - config - .rpc_client - .send_and_confirm_transaction(&Transaction::new_signed_with_payer( - &[system_instruction::transfer( - &payer.pubkey(), - &multisig_pubkey, - excess_lamports, - )], - Some(&payer.pubkey()), - &[&payer], - config.rpc_client.get_latest_blockhash().await.unwrap(), - )) - .await - .unwrap(); - - exec_test_cmd( - &config, - &[ - "spl-token", - CommandName::WithdrawExcessLamports.into(), - &multisig_pubkey.to_string(), - &receiver.pubkey().to_string(), - "--owner", - &multisig_pubkey.to_string(), - "--multisig-signer", - multisig_paths[0].path().to_str().unwrap(), - "--multisig-signer", - multisig_paths[1].path().to_str().unwrap(), - "--multisig-signer", - multisig_paths[2].path().to_str().unwrap(), - "--fee-payer", - fee_payer_keypair_file.path().to_str().unwrap(), - "--program-id", - &program_id.to_string(), - ], - ) - .await - .unwrap(); - - assert_eq!( - excess_lamports, - config - .rpc_client - .get_balance(&receiver.pubkey()) - .await - .unwrap() - ); - } - - #[tokio::test] - #[serial] - async fn withdraw_excess_lamports_from_mint() { - let (test_validator, payer) = new_validator_for_test().await; - let program_id = &spl_token_2022::id(); - let config = test_config_with_default_signer(&test_validator, &payer, program_id); - let owner_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(&payer, &owner_keypair_file).unwrap(); - - let receiver = Keypair::new(); - - let token_keypair = Keypair::new(); - let token_path = NamedTempFile::new().unwrap(); - write_keypair_file(&token_keypair, &token_path).unwrap(); - let token_pubkey = token_keypair.pubkey(); - - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::CreateToken.into(), - token_path.path().to_str().unwrap(), - "--program-id", - &program_id.to_string(), - ], - ) - .await - .unwrap(); - - let excess_lamports = 4000 * 1_000_000_000; - config - .rpc_client - .send_and_confirm_transaction(&Transaction::new_signed_with_payer( - &[system_instruction::transfer( - &payer.pubkey(), - &token_pubkey, - excess_lamports, - )], - Some(&payer.pubkey()), - &[&payer], - config.rpc_client.get_latest_blockhash().await.unwrap(), - )) - .await - .unwrap(); - - exec_test_cmd( - &config, - &[ - "spl-token", - CommandName::WithdrawExcessLamports.into(), - &token_pubkey.to_string(), - &receiver.pubkey().to_string(), - "--owner", - owner_keypair_file.path().to_str().unwrap(), - "--program-id", - &program_id.to_string(), - ], - ) - .await - .unwrap(); - - assert_eq!( - excess_lamports, - config - .rpc_client - .get_balance(&receiver.pubkey()) - .await - .unwrap() - ); - } - - #[tokio::test] - #[serial] - async fn withdraw_excess_lamports_from_account() { - let (test_validator, payer) = new_validator_for_test().await; - let program_id = &spl_token_2022::id(); - let config = test_config_with_default_signer(&test_validator, &payer, program_id); - let owner_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(&payer, &owner_keypair_file).unwrap(); - - let receiver = Keypair::new(); - - let token_keypair = Keypair::new(); - let token_path = NamedTempFile::new().unwrap(); - write_keypair_file(&token_keypair, &token_path).unwrap(); - let token_pubkey = token_keypair.pubkey(); - - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::CreateToken.into(), - token_path.path().to_str().unwrap(), - "--program-id", - &program_id.to_string(), - ], - ) - .await - .unwrap(); - - let excess_lamports = 4000 * 1_000_000_000; - let token_account = - create_associated_account(&config, &payer, &token_pubkey, &payer.pubkey()).await; - - config - .rpc_client - .send_and_confirm_transaction(&Transaction::new_signed_with_payer( - &[system_instruction::transfer( - &payer.pubkey(), - &token_account, - excess_lamports, - )], - Some(&payer.pubkey()), - &[&payer], - config.rpc_client.get_latest_blockhash().await.unwrap(), - )) - .await - .unwrap(); - - exec_test_cmd( - &config, - &[ - "spl-token", - CommandName::WithdrawExcessLamports.into(), - &token_account.to_string(), - &receiver.pubkey().to_string(), - "--owner", - owner_keypair_file.path().to_str().unwrap(), - "--program-id", - &program_id.to_string(), - ], - ) - .await - .unwrap(); - - assert_eq!( - excess_lamports, - config - .rpc_client - .get_balance(&receiver.pubkey()) - .await - .unwrap() - ); - } - - #[tokio::test] - #[serial] - async fn metadata_pointer() { - let (test_validator, payer) = new_validator_for_test().await; - let program_id = spl_token_2022::id(); - let config = test_config_with_default_signer(&test_validator, &payer, &program_id); - let metadata_address = Pubkey::new_unique(); - - let result = process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::CreateToken.into(), - "--program-id", - &program_id.to_string(), - "--metadata-address", - &metadata_address.to_string(), - ], - ) - .await; - - let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); - let mint = Pubkey::from_str(value["commandOutput"]["address"].as_str().unwrap()).unwrap(); - let account = config.rpc_client.get_account(&mint).await.unwrap(); - let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - - let extension = mint_state.get_extension::().unwrap(); - - assert_eq!( - extension.metadata_address, - Some(metadata_address).try_into().unwrap() - ); - - let new_metadata_address = Pubkey::new_unique(); - - let _new_result = process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::UpdateMetadataAddress.into(), - &mint.to_string(), - &new_metadata_address.to_string(), - ], - ) - .await; - - let new_account = config.rpc_client.get_account(&mint).await.unwrap(); - let new_mint_state = StateWithExtensionsOwned::::unpack(new_account.data).unwrap(); - - let new_extension = new_mint_state.get_extension::().unwrap(); - - assert_eq!( - new_extension.metadata_address, - Some(new_metadata_address).try_into().unwrap() - ); - - let _result_with_disable = process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::UpdateMetadataAddress.into(), - &mint.to_string(), - "--disable", - ], - ) - .await; - - let new_account_disbale = config.rpc_client.get_account(&mint).await.unwrap(); - let new_mint_state_disable = - StateWithExtensionsOwned::::unpack(new_account_disbale.data).unwrap(); - - let new_extension_disable = new_mint_state_disable - .get_extension::() - .unwrap(); - - assert_eq!( - new_extension_disable.metadata_address, - None.try_into().unwrap() - ); - } - - #[tokio::test] - #[serial] - async fn transfer_hook() { - let (test_validator, payer) = new_validator_for_test().await; - let program_id = spl_token_2022::id(); - let mut config = test_config_with_default_signer(&test_validator, &payer, &program_id); - let transfer_hook_program_id = Pubkey::new_unique(); - - let result = process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::CreateToken.into(), - "--program-id", - &program_id.to_string(), - "--transfer-hook", - &transfer_hook_program_id.to_string(), - ], - ) - .await; - - let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); - let mint = Pubkey::from_str(value["commandOutput"]["address"].as_str().unwrap()).unwrap(); - let account = config.rpc_client.get_account(&mint).await.unwrap(); - let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = mint_state.get_extension::().unwrap(); - - assert_eq!( - extension.program_id, - Some(transfer_hook_program_id).try_into().unwrap() - ); - - let new_transfer_hook_program_id = Pubkey::new_unique(); - - let _new_result = process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::SetTransferHook.into(), - &mint.to_string(), - &new_transfer_hook_program_id.to_string(), - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&mint).await.unwrap(); - let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = mint_state.get_extension::().unwrap(); - - assert_eq!( - extension.program_id, - Some(new_transfer_hook_program_id).try_into().unwrap() - ); - - // Make sure that parsing transfer hook accounts works - let real_program_client = config.program_client; - let blockhash = Hash::default(); - let program_client: Arc> = Arc::new( - ProgramOfflineClient::new(blockhash, ProgramRpcClientSendTransaction), - ); - config.program_client = program_client; - let _result = exec_test_cmd( - &config, - &[ - "spl-token", - CommandName::Transfer.into(), - &mint.to_string(), - "10", - &Pubkey::new_unique().to_string(), - "--blockhash", - &blockhash.to_string(), - "--nonce", - &Pubkey::new_unique().to_string(), - "--nonce-authority", - &Pubkey::new_unique().to_string(), - "--sign-only", - "--mint-decimals", - &format!("{}", TEST_DECIMALS), - "--from", - &Pubkey::new_unique().to_string(), - "--owner", - &Pubkey::new_unique().to_string(), - "--transfer-hook-account", - &format!("{}:readonly", Pubkey::new_unique()), - "--transfer-hook-account", - &format!("{}:writable", Pubkey::new_unique()), - "--transfer-hook-account", - &format!("{}:readonly-signer", Pubkey::new_unique()), - "--transfer-hook-account", - &format!("{}:writable-signer", Pubkey::new_unique()), - ], - ) - .await - .unwrap(); - - config.program_client = real_program_client; - let _result_with_disable = process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::SetTransferHook.into(), - &mint.to_string(), - "--disable", - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&mint).await.unwrap(); - let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = mint_state.get_extension::().unwrap(); - - assert_eq!(extension.program_id, None.try_into().unwrap()); - } - - #[tokio::test] - #[serial] - async fn metadata() { - let (test_validator, payer) = new_validator_for_test().await; - let program_id = spl_token_2022::id(); - let config = test_config_with_default_signer(&test_validator, &payer, &program_id); - let name = "this"; - let symbol = "is"; - let uri = "METADATA!"; - - let result = process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::CreateToken.into(), - "--program-id", - &program_id.to_string(), - "--enable-metadata", - ], - ) - .await; - - let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); - let mint = Pubkey::from_str(value["commandOutput"]["address"].as_str().unwrap()).unwrap(); - let account = config.rpc_client.get_account(&mint).await.unwrap(); - let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - - let extension = mint_state.get_extension::().unwrap(); - assert_eq!(extension.metadata_address, Some(mint).try_into().unwrap()); - - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::InitializeMetadata.into(), - &mint.to_string(), - name, - symbol, - uri, - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&mint).await.unwrap(); - let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let fetched_metadata = mint_state - .get_variable_len_extension::() - .unwrap(); - assert_eq!(fetched_metadata.name, name); - assert_eq!(fetched_metadata.symbol, symbol); - assert_eq!(fetched_metadata.uri, uri); - assert_eq!(fetched_metadata.mint, mint); - assert_eq!( - fetched_metadata.update_authority, - Some(payer.pubkey()).try_into().unwrap() - ); - assert_eq!(fetched_metadata.additional_metadata, []); - - // update canonical field - let new_value = "THIS!"; - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::UpdateMetadata.into(), - &mint.to_string(), - "NAME", - new_value, - ], - ) - .await - .unwrap(); - let account = config.rpc_client.get_account(&mint).await.unwrap(); - let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let fetched_metadata = mint_state - .get_variable_len_extension::() - .unwrap(); - assert_eq!(fetched_metadata.name, new_value); - - // add new field - let field = "My field!"; - let value = "Try and stop me"; - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::UpdateMetadata.into(), - &mint.to_string(), - field, - value, - ], - ) - .await - .unwrap(); - let account = config.rpc_client.get_account(&mint).await.unwrap(); - let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let fetched_metadata = mint_state - .get_variable_len_extension::() - .unwrap(); - assert_eq!( - fetched_metadata.additional_metadata, - [(field.to_string(), value.to_string())] - ); +#[tokio::main] +async fn main() -> Result<(), Error> { + let default_decimals = format!("{}", spl_token_2022::native_mint::DECIMALS); + let minimum_signers_help = minimum_signers_help_string(); + let multisig_member_help = multisig_member_help_string(); + let app_matches = app( + &default_decimals, + &minimum_signers_help, + &multisig_member_help, + ) + .get_matches(); - // remove it - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::UpdateMetadata.into(), - &mint.to_string(), - field, - "--remove", - ], - ) - .await - .unwrap(); - let account = config.rpc_client.get_account(&mint).await.unwrap(); - let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let fetched_metadata = mint_state - .get_variable_len_extension::() - .unwrap(); - assert_eq!(fetched_metadata.additional_metadata, []); + let mut wallet_manager = None; + let mut bulk_signers: Vec> = Vec::new(); - // fail to remove name - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::UpdateMetadata.into(), - &mint.to_string(), - "name", - "--remove", - ], - ) - .await - .unwrap_err(); + let (sub_command, sub_matches) = app_matches.subcommand(); + let sub_command = CommandName::from_str(sub_command).unwrap(); + let matches = sub_matches.unwrap(); - // update authority - process_test_command( - &config, - &payer, - &[ - "spl-token", - CommandName::Authorize.into(), - &mint.to_string(), - "metadata", - &mint.to_string(), - ], - ) - .await - .unwrap(); + let mut multisigner_ids = Vec::new(); + let config = Config::new( + matches, + &mut wallet_manager, + &mut bulk_signers, + &mut multisigner_ids, + ) + .await; - let account = config.rpc_client.get_account(&mint).await.unwrap(); - let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let fetched_metadata = mint_state - .get_variable_len_extension::() - .unwrap(); - assert_eq!( - fetched_metadata.update_authority, - Some(mint).try_into().unwrap() - ); - } + solana_logger::setup_with_default("solana=info"); + let result = + process_command(&sub_command, matches, &config, wallet_manager, bulk_signers).await?; + println!("{}", result); + Ok(()) } diff --git a/token/cli/src/output.rs b/token/cli/src/output.rs index 9b54bbe689e..a5cb902b23a 100644 --- a/token/cli/src/output.rs +++ b/token/cli/src/output.rs @@ -1,3 +1,4 @@ +#![allow(clippy::arithmetic_side_effects)] use { crate::{config::Config, sort::UnsupportedAccount}, console::{style, Emoji}, diff --git a/token/cli/src/sort.rs b/token/cli/src/sort.rs index 463e63d72fb..09423694519 100644 --- a/token/cli/src/sort.rs +++ b/token/cli/src/sort.rs @@ -1,7 +1,8 @@ +#![allow(clippy::arithmetic_side_effects)] use { crate::{ + clap_app::Error, output::{CliTokenAccount, CliTokenAccounts}, - Error, }, serde::{Deserialize, Serialize}, solana_account_decoder::{parse_token::TokenAccountType, UiAccountData}, diff --git a/token/cli/tests/command.rs b/token/cli/tests/command.rs new file mode 100644 index 00000000000..acfab9677c9 --- /dev/null +++ b/token/cli/tests/command.rs @@ -0,0 +1,3621 @@ +#![allow(clippy::arithmetic_side_effects)] +use { + serial_test::serial, + solana_cli_output::OutputFormat, + solana_client::rpc_request::TokenAccountsFilter, + solana_sdk::{ + bpf_loader_upgradeable, feature_set, + hash::Hash, + program_option::COption, + program_pack::Pack, + pubkey::Pubkey, + signature::{write_keypair_file, Keypair, Signer}, + system_instruction, system_program, + transaction::Transaction, + }, + solana_test_validator::{TestValidator, TestValidatorGenesis, UpgradeableProgramInfo}, + spl_associated_token_account::get_associated_token_address_with_program_id, + spl_token_2022::{ + extension::{ + confidential_transfer::{ConfidentialTransferAccount, ConfidentialTransferMint}, + cpi_guard::CpiGuard, + default_account_state::DefaultAccountState, + interest_bearing_mint::InterestBearingConfig, + memo_transfer::MemoTransfer, + metadata_pointer::MetadataPointer, + non_transferable::NonTransferable, + transfer_fee::{TransferFeeAmount, TransferFeeConfig}, + transfer_hook::TransferHook, + BaseStateWithExtensions, StateWithExtensionsOwned, + }, + instruction::create_native_mint, + solana_zk_token_sdk::zk_token_elgamal::pod::ElGamalPubkey, + state::{Account, AccountState, Mint, Multisig}, + }, + spl_token_cli::{ + clap_app::*, + command::{process_command, CommandResult}, + config::Config, + }, + spl_token_client::{ + client::{ + ProgramClient, ProgramOfflineClient, ProgramRpcClient, ProgramRpcClientSendTransaction, + }, + token::Token, + }, + spl_token_metadata_interface::state::TokenMetadata, + std::{ffi::OsString, path::PathBuf, str::FromStr, sync::Arc}, + tempfile::NamedTempFile, +}; + +fn clone_keypair(keypair: &Keypair) -> Keypair { + Keypair::from_bytes(&keypair.to_bytes()).unwrap() +} + +const TEST_DECIMALS: u8 = 9; + +async fn new_validator_for_test() -> (TestValidator, Keypair) { + solana_logger::setup(); + let mut test_validator_genesis = TestValidatorGenesis::default(); + test_validator_genesis.add_upgradeable_programs_with_path(&[ + UpgradeableProgramInfo { + program_id: spl_token::id(), + loader: bpf_loader_upgradeable::id(), + program_path: PathBuf::from("../../target/deploy/spl_token.so"), + upgrade_authority: Pubkey::new_unique(), + }, + UpgradeableProgramInfo { + program_id: spl_associated_token_account::id(), + loader: bpf_loader_upgradeable::id(), + program_path: PathBuf::from("../../target/deploy/spl_associated_token_account.so"), + upgrade_authority: Pubkey::new_unique(), + }, + UpgradeableProgramInfo { + program_id: spl_token_2022::id(), + loader: bpf_loader_upgradeable::id(), + program_path: PathBuf::from("../../target/deploy/spl_token_2022.so"), + upgrade_authority: Pubkey::new_unique(), + }, + ]); + // TODO Remove this once the Range Proof cost goes under 200k compute units + test_validator_genesis.deactivate_features(&[feature_set::native_programs_consume_cu::id()]); + test_validator_genesis.start_async().await +} + +fn test_config_with_default_signer<'a>( + test_validator: &TestValidator, + payer: &Keypair, + program_id: &Pubkey, +) -> Config<'a> { + let websocket_url = test_validator.rpc_pubsub_url(); + let rpc_client = Arc::new(test_validator.get_async_rpc_client()); + let program_client: Arc> = Arc::new( + ProgramRpcClient::new(rpc_client.clone(), ProgramRpcClientSendTransaction), + ); + Config { + rpc_client, + program_client, + websocket_url, + output_format: OutputFormat::JsonCompact, + fee_payer: Some(Arc::new(clone_keypair(payer))), + default_signer: Some(Arc::new(clone_keypair(payer))), + nonce_account: None, + nonce_authority: None, + nonce_blockhash: None, + sign_only: false, + dump_transaction_message: false, + multisigner_pubkeys: vec![], + program_id: *program_id, + restrict_to_program_id: true, + } +} + +fn test_config_without_default_signer<'a>( + test_validator: &TestValidator, + program_id: &Pubkey, +) -> Config<'a> { + let websocket_url = test_validator.rpc_pubsub_url(); + let rpc_client = Arc::new(test_validator.get_async_rpc_client()); + let program_client: Arc> = Arc::new( + ProgramRpcClient::new(rpc_client.clone(), ProgramRpcClientSendTransaction), + ); + Config { + rpc_client, + program_client, + websocket_url, + output_format: OutputFormat::JsonCompact, + fee_payer: None, + default_signer: None, + nonce_account: None, + nonce_authority: None, + nonce_blockhash: None, + sign_only: false, + dump_transaction_message: false, + multisigner_pubkeys: vec![], + program_id: *program_id, + restrict_to_program_id: true, + } +} + +async fn create_nonce(config: &Config<'_>, authority: &Keypair) -> Pubkey { + let nonce = Keypair::new(); + + let nonce_rent = config + .rpc_client + .get_minimum_balance_for_rent_exemption(solana_sdk::nonce::State::size()) + .await + .unwrap(); + let instr = system_instruction::create_nonce_account( + &authority.pubkey(), + &nonce.pubkey(), + &authority.pubkey(), // Make the fee payer the nonce account authority + nonce_rent, + ); + + let blockhash = config.rpc_client.get_latest_blockhash().await.unwrap(); + let tx = Transaction::new_signed_with_payer( + &instr, + Some(&authority.pubkey()), + &[&nonce, authority], + blockhash, + ); + + config + .rpc_client + .send_and_confirm_transaction(&tx) + .await + .unwrap(); + nonce.pubkey() +} + +async fn do_create_native_mint(config: &Config<'_>, program_id: &Pubkey, payer: &Keypair) { + if program_id == &spl_token_2022::id() { + let native_mint = spl_token_2022::native_mint::id(); + if config.rpc_client.get_account(&native_mint).await.is_err() { + let transaction = Transaction::new_signed_with_payer( + &[create_native_mint(program_id, &payer.pubkey()).unwrap()], + Some(&payer.pubkey()), + &[payer], + config.rpc_client.get_latest_blockhash().await.unwrap(), + ); + config + .rpc_client + .send_and_confirm_transaction(&transaction) + .await + .unwrap(); + } + } +} + +async fn create_token(config: &Config<'_>, payer: &Keypair) -> Pubkey { + let token = Keypair::new(); + let token_keypair_file = NamedTempFile::new().unwrap(); + write_keypair_file(&token, &token_keypair_file).unwrap(); + process_test_command( + config, + payer, + &[ + "spl-token", + CommandName::CreateToken.into(), + token_keypair_file.path().to_str().unwrap(), + ], + ) + .await + .unwrap(); + token.pubkey() +} + +async fn create_interest_bearing_token( + config: &Config<'_>, + payer: &Keypair, + rate_bps: i16, +) -> Pubkey { + let token = Keypair::new(); + let token_keypair_file = NamedTempFile::new().unwrap(); + write_keypair_file(&token, &token_keypair_file).unwrap(); + process_test_command( + config, + payer, + &[ + "spl-token", + CommandName::CreateToken.into(), + token_keypair_file.path().to_str().unwrap(), + "--interest-rate", + &rate_bps.to_string(), + ], + ) + .await + .unwrap(); + token.pubkey() +} + +async fn create_auxiliary_account(config: &Config<'_>, payer: &Keypair, mint: Pubkey) -> Pubkey { + let auxiliary = Keypair::new(); + let auxiliary_keypair_file = NamedTempFile::new().unwrap(); + write_keypair_file(&auxiliary, &auxiliary_keypair_file).unwrap(); + process_test_command( + config, + payer, + &[ + "spl-token", + CommandName::CreateAccount.into(), + &mint.to_string(), + auxiliary_keypair_file.path().to_str().unwrap(), + ], + ) + .await + .unwrap(); + auxiliary.pubkey() +} + +async fn create_associated_account( + config: &Config<'_>, + payer: &Keypair, + mint: &Pubkey, + owner: &Pubkey, +) -> Pubkey { + process_test_command( + config, + payer, + &[ + "spl-token", + CommandName::CreateAccount.into(), + &mint.to_string(), + "--owner", + &owner.to_string(), + ], + ) + .await + .unwrap(); + get_associated_token_address_with_program_id(owner, mint, &config.program_id) +} + +async fn mint_tokens( + config: &Config<'_>, + payer: &Keypair, + mint: Pubkey, + ui_amount: f64, + recipient: Pubkey, +) -> CommandResult { + process_test_command( + config, + payer, + &[ + "spl-token", + CommandName::Mint.into(), + &mint.to_string(), + &ui_amount.to_string(), + &recipient.to_string(), + ], + ) + .await +} + +async fn process_test_command(config: &Config<'_>, payer: &Keypair, args: I) -> CommandResult +where + I: IntoIterator, + T: Into + Clone, +{ + let default_decimals = format!("{}", spl_token_2022::native_mint::DECIMALS); + let minimum_signers_help = minimum_signers_help_string(); + let multisig_member_help = multisig_member_help_string(); + + let app_matches = app( + &default_decimals, + &minimum_signers_help, + &multisig_member_help, + ) + .get_matches_from(args); + let (sub_command, sub_matches) = app_matches.subcommand(); + let sub_command = CommandName::from_str(sub_command).unwrap(); + let matches = sub_matches.unwrap(); + + let wallet_manager = None; + let bulk_signers: Vec> = vec![Arc::new(clone_keypair(payer))]; + process_command(&sub_command, matches, config, wallet_manager, bulk_signers).await +} + +async fn exec_test_cmd(config: &Config<'_>, args: &[&str]) -> CommandResult { + let default_decimals = format!("{}", spl_token_2022::native_mint::DECIMALS); + let minimum_signers_help = minimum_signers_help_string(); + let multisig_member_help = multisig_member_help_string(); + + let app_matches = app( + &default_decimals, + &minimum_signers_help, + &multisig_member_help, + ) + .get_matches_from(args); + let (sub_command, sub_matches) = app_matches.subcommand(); + let sub_command = CommandName::from_str(sub_command).unwrap(); + let matches = sub_matches.unwrap(); + + let mut wallet_manager = None; + let mut bulk_signers: Vec> = Vec::new(); + let mut multisigner_ids = Vec::new(); + + let config = Config::new_with_clients_and_ws_url( + matches, + &mut wallet_manager, + &mut bulk_signers, + &mut multisigner_ids, + config.rpc_client.clone(), + config.program_client.clone(), + config.websocket_url.clone(), + ) + .await; + + process_command(&sub_command, matches, &config, wallet_manager, bulk_signers).await +} + +#[tokio::test] +#[serial] +async fn create_token_default() { + let (test_validator, payer) = new_validator_for_test().await; + for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { + let config = test_config_with_default_signer(&test_validator, &payer, program_id); + let result = process_test_command( + &config, + &payer, + &["spl-token", CommandName::CreateToken.into()], + ) + .await; + let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); + let mint = Pubkey::from_str(value["commandOutput"]["address"].as_str().unwrap()).unwrap(); + let account = config.rpc_client.get_account(&mint).await.unwrap(); + assert_eq!(account.owner, *program_id); + } +} + +#[tokio::test] +#[serial] +async fn create_token_interest_bearing() { + let (test_validator, payer) = new_validator_for_test().await; + let config = test_config_with_default_signer(&test_validator, &payer, &spl_token_2022::id()); + let rate_bps: i16 = 100; + let result = process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::CreateToken.into(), + "--interest-rate", + &rate_bps.to_string(), + ], + ) + .await; + let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); + let mint = Pubkey::from_str(value["commandOutput"]["address"].as_str().unwrap()).unwrap(); + let account = config.rpc_client.get_account(&mint).await.unwrap(); + let mint_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + let extension = mint_account + .get_extension::() + .unwrap(); + assert_eq!(account.owner, spl_token_2022::id()); + assert_eq!(i16::from(extension.current_rate), rate_bps); + assert_eq!( + Option::::from(extension.rate_authority), + Some(payer.pubkey()) + ); +} + +#[tokio::test] +#[serial] +async fn set_interest_rate() { + let (test_validator, payer) = new_validator_for_test().await; + let config = test_config_with_default_signer(&test_validator, &payer, &spl_token_2022::id()); + let initial_rate: i16 = 100; + let new_rate: i16 = 300; + let token = create_interest_bearing_token(&config, &payer, initial_rate).await; + let account = config.rpc_client.get_account(&token).await.unwrap(); + let mint_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + let extension = mint_account + .get_extension::() + .unwrap(); + assert_eq!(account.owner, spl_token_2022::id()); + assert_eq!(i16::from(extension.current_rate), initial_rate); + + let result = process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::SetInterestRate.into(), + &token.to_string(), + &new_rate.to_string(), + ], + ) + .await; + let _value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); + let account = config.rpc_client.get_account(&token).await.unwrap(); + let mint_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + let extension = mint_account + .get_extension::() + .unwrap(); + assert_eq!(i16::from(extension.current_rate), new_rate); +} + +#[tokio::test] +#[serial] +async fn supply() { + let (test_validator, payer) = new_validator_for_test().await; + for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { + let config = test_config_with_default_signer(&test_validator, &payer, program_id); + let token = create_token(&config, &payer).await; + let result = process_test_command( + &config, + &payer, + &["spl-token", CommandName::Supply.into(), &token.to_string()], + ) + .await; + let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); + assert_eq!(value["amount"], "0"); + assert_eq!(value["uiAmountString"], "0"); + } +} + +#[tokio::test] +#[serial] +async fn create_account_default() { + let (test_validator, payer) = new_validator_for_test().await; + for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { + let config = test_config_with_default_signer(&test_validator, &payer, program_id); + let token = create_token(&config, &payer).await; + let result = process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::CreateAccount.into(), + &token.to_string(), + ], + ) + .await; + result.unwrap(); + } +} + +#[tokio::test] +#[serial] +async fn account_info() { + let (test_validator, payer) = new_validator_for_test().await; + for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { + let config = test_config_with_default_signer(&test_validator, &payer, program_id); + let token = create_token(&config, &payer).await; + let _account = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; + let result = process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::AccountInfo.into(), + &token.to_string(), + ], + ) + .await; + let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); + let account = get_associated_token_address_with_program_id( + &payer.pubkey(), + &token, + &config.program_id, + ); + assert_eq!(value["address"], account.to_string()); + assert_eq!(value["mint"], token.to_string()); + assert_eq!(value["isAssociated"], true); + assert_eq!(value["isNative"], false); + assert_eq!(value["owner"], payer.pubkey().to_string()); + assert_eq!(value["state"], "initialized"); + } +} + +#[tokio::test] +#[serial] +async fn balance() { + let (test_validator, payer) = new_validator_for_test().await; + for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { + let config = test_config_with_default_signer(&test_validator, &payer, program_id); + let token = create_token(&config, &payer).await; + let _account = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; + let result = process_test_command( + &config, + &payer, + &["spl-token", CommandName::Balance.into(), &token.to_string()], + ) + .await; + let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); + assert_eq!(value["amount"], "0"); + assert_eq!(value["uiAmountString"], "0"); + } +} + +#[tokio::test] +#[serial] +async fn mint() { + let (test_validator, payer) = new_validator_for_test().await; + for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { + let config = test_config_with_default_signer(&test_validator, &payer, program_id); + let token = create_token(&config, &payer).await; + let account = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; + let mut amount = 0; + + // mint via implicit owner + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Mint.into(), + &token.to_string(), + "1", + ], + ) + .await + .unwrap(); + amount += spl_token::ui_amount_to_amount(1.0, TEST_DECIMALS); + + let account_data = config.rpc_client.get_account(&account).await.unwrap(); + let token_account = StateWithExtensionsOwned::::unpack(account_data.data).unwrap(); + assert_eq!(token_account.base.amount, amount); + assert_eq!(token_account.base.mint, token); + assert_eq!(token_account.base.owner, payer.pubkey()); + + // mint via explicit recipient + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Mint.into(), + &token.to_string(), + "1", + &account.to_string(), + ], + ) + .await + .unwrap(); + amount += spl_token::ui_amount_to_amount(1.0, TEST_DECIMALS); + + let account_data = config.rpc_client.get_account(&account).await.unwrap(); + let token_account = StateWithExtensionsOwned::::unpack(account_data.data).unwrap(); + assert_eq!(token_account.base.amount, amount); + assert_eq!(token_account.base.mint, token); + assert_eq!(token_account.base.owner, payer.pubkey()); + + // mint via explicit owner + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Mint.into(), + &token.to_string(), + "1", + "--recipient-owner", + &payer.pubkey().to_string(), + ], + ) + .await + .unwrap(); + amount += spl_token::ui_amount_to_amount(1.0, TEST_DECIMALS); + + let account_data = config.rpc_client.get_account(&account).await.unwrap(); + let token_account = StateWithExtensionsOwned::::unpack(account_data.data).unwrap(); + assert_eq!(token_account.base.amount, amount); + assert_eq!(token_account.base.mint, token); + assert_eq!(token_account.base.owner, payer.pubkey()); + } +} + +#[tokio::test] +#[serial] +async fn balance_after_mint() { + let (test_validator, payer) = new_validator_for_test().await; + for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { + let config = test_config_with_default_signer(&test_validator, &payer, program_id); + let token = create_token(&config, &payer).await; + let account = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; + let ui_amount = 100.0; + mint_tokens(&config, &payer, token, ui_amount, account) + .await + .unwrap(); + let result = process_test_command( + &config, + &payer, + &["spl-token", CommandName::Balance.into(), &token.to_string()], + ) + .await; + let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); + let amount = spl_token::ui_amount_to_amount(ui_amount, TEST_DECIMALS); + assert_eq!(value["amount"], format!("{}", amount)); + assert_eq!(value["uiAmountString"], format!("{}", ui_amount)); + } +} +#[tokio::test] +#[serial] +async fn balance_after_mint_with_owner() { + let (test_validator, payer) = new_validator_for_test().await; + for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { + let config = test_config_with_default_signer(&test_validator, &payer, program_id); + let token = create_token(&config, &payer).await; + let account = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; + let ui_amount = 100.0; + mint_tokens(&config, &payer, token, ui_amount, account) + .await + .unwrap(); + let config = test_config_without_default_signer(&test_validator, program_id); + let result = process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Balance.into(), + &token.to_string(), + "--owner", + &payer.pubkey().to_string(), + ], + ) + .await; + let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); + let amount = spl_token::ui_amount_to_amount(ui_amount, TEST_DECIMALS); + assert_eq!(value["amount"], format!("{}", amount)); + assert_eq!(value["uiAmountString"], format!("{}", ui_amount)); + } +} + +#[tokio::test] +#[serial] +async fn accounts() { + let (test_validator, payer) = new_validator_for_test().await; + for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { + let config = test_config_with_default_signer(&test_validator, &payer, program_id); + let token1 = create_token(&config, &payer).await; + let _account1 = create_associated_account(&config, &payer, &token1, &payer.pubkey()).await; + let token2 = create_token(&config, &payer).await; + let _account2 = create_associated_account(&config, &payer, &token2, &payer.pubkey()).await; + let token3 = create_token(&config, &payer).await; + let result = process_test_command( + &config, + &payer, + &["spl-token", CommandName::Accounts.into()], + ) + .await + .unwrap(); + assert!(result.contains(&token1.to_string())); + assert!(result.contains(&token2.to_string())); + assert!(!result.contains(&token3.to_string())); + } +} + +#[tokio::test] +#[serial] +async fn accounts_with_owner() { + let (test_validator, payer) = new_validator_for_test().await; + for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { + let config = test_config_with_default_signer(&test_validator, &payer, program_id); + let token1 = create_token(&config, &payer).await; + let _account1 = create_associated_account(&config, &payer, &token1, &payer.pubkey()).await; + let token2 = create_token(&config, &payer).await; + let _account2 = create_associated_account(&config, &payer, &token2, &payer.pubkey()).await; + let token3 = create_token(&config, &payer).await; + let config = test_config_without_default_signer(&test_validator, program_id); + let result = process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Accounts.into(), + "--owner", + &payer.pubkey().to_string(), + ], + ) + .await + .unwrap(); + assert!(result.contains(&token1.to_string())); + assert!(result.contains(&token2.to_string())); + assert!(!result.contains(&token3.to_string())); + } +} + +#[tokio::test] +#[serial] +async fn wrap() { + let (test_validator, payer) = new_validator_for_test().await; + for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { + let config = test_config_with_default_signer(&test_validator, &payer, program_id); + let native_mint = *Token::new_native( + config.program_client.clone(), + program_id, + config.fee_payer().unwrap().clone(), + ) + .get_address(); + do_create_native_mint(&config, program_id, &payer).await; + let _result = process_test_command( + &config, + &payer, + &["spl-token", CommandName::Wrap.into(), "0.5"], + ) + .await + .unwrap(); + let account = get_associated_token_address_with_program_id( + &payer.pubkey(), + &native_mint, + &config.program_id, + ); + let account = config.rpc_client.get_account(&account).await.unwrap(); + let token_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + assert_eq!(token_account.base.mint, native_mint); + assert_eq!(token_account.base.owner, payer.pubkey()); + assert!(token_account.base.is_native()); + } +} + +#[tokio::test] +#[serial] +async fn unwrap() { + let (test_validator, payer) = new_validator_for_test().await; + for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { + let config = test_config_with_default_signer(&test_validator, &payer, program_id); + let native_mint = *Token::new_native( + config.program_client.clone(), + program_id, + config.fee_payer().unwrap().clone(), + ) + .get_address(); + do_create_native_mint(&config, program_id, &payer).await; + let account = get_associated_token_address_with_program_id( + &payer.pubkey(), + &native_mint, + &config.program_id, + ); + let _result = process_test_command( + &config, + &payer, + &["spl-token", CommandName::Wrap.into(), "0.5"], + ) + .await + .unwrap(); + let result = process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Unwrap.into(), + &account.to_string(), + ], + ) + .await; + result.unwrap(); + config.rpc_client.get_account(&account).await.unwrap_err(); + } +} + +#[tokio::test] +#[serial] +async fn transfer() { + let (test_validator, payer) = new_validator_for_test().await; + for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { + let config = test_config_with_default_signer(&test_validator, &payer, program_id); + let token = create_token(&config, &payer).await; + let source = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; + let destination = create_auxiliary_account(&config, &payer, token).await; + let ui_amount = 100.0; + mint_tokens(&config, &payer, token, ui_amount, source) + .await + .unwrap(); + let result = process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Transfer.into(), + &token.to_string(), + "10", + &destination.to_string(), + ], + ) + .await; + result.unwrap(); + + let account = config.rpc_client.get_account(&source).await.unwrap(); + let token_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + let amount = spl_token::ui_amount_to_amount(90.0, TEST_DECIMALS); + assert_eq!(token_account.base.amount, amount); + let account = config.rpc_client.get_account(&destination).await.unwrap(); + let token_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + let amount = spl_token::ui_amount_to_amount(10.0, TEST_DECIMALS); + assert_eq!(token_account.base.amount, amount); + } +} + +#[tokio::test] +#[serial] +async fn transfer_fund_recipient() { + let (test_validator, payer) = new_validator_for_test().await; + for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { + let config = test_config_with_default_signer(&test_validator, &payer, program_id); + let token = create_token(&config, &payer).await; + let source = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; + let recipient = Keypair::new().pubkey().to_string(); + let ui_amount = 100.0; + mint_tokens(&config, &payer, token, ui_amount, source) + .await + .unwrap(); + let result = process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Transfer.into(), + "--fund-recipient", + "--allow-unfunded-recipient", + &token.to_string(), + "10", + &recipient, + ], + ) + .await; + result.unwrap(); + + let account = config.rpc_client.get_account(&source).await.unwrap(); + let token_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + assert_eq!( + token_account.base.amount, + spl_token::ui_amount_to_amount(90.0, TEST_DECIMALS) + ); + } +} + +#[tokio::test] +#[serial] +async fn transfer_non_standard_recipient() { + let (test_validator, payer) = new_validator_for_test().await; + for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { + for other_program_id in VALID_TOKEN_PROGRAM_IDS + .iter() + .filter(|id| *id != program_id) + { + let mut config = + test_config_with_default_signer(&test_validator, &payer, other_program_id); + let wrong_program_token = create_token(&config, &payer).await; + let wrong_program_account = + create_associated_account(&config, &payer, &wrong_program_token, &payer.pubkey()) + .await; + config.program_id = *program_id; + let config = config; + + let token = create_token(&config, &payer).await; + let source = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; + let recipient = Keypair::new().pubkey(); + let recipient_token_account = get_associated_token_address_with_program_id( + &recipient, + &token, + &config.program_id, + ); + let system_token_account = get_associated_token_address_with_program_id( + &system_program::id(), + &token, + &config.program_id, + ); + let amount = 100; + mint_tokens(&config, &payer, token, amount as f64, source) + .await + .unwrap(); + + // transfer fails to unfunded recipient without flag + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Transfer.into(), + "--fund-recipient", + &token.to_string(), + "1", + &recipient.to_string(), + ], + ) + .await + .unwrap_err(); + + // with unfunded flag, transfer goes through + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Transfer.into(), + "--fund-recipient", + "--allow-unfunded-recipient", + &token.to_string(), + "1", + &recipient.to_string(), + ], + ) + .await + .unwrap(); + let account = config + .rpc_client + .get_account(&recipient_token_account) + .await + .unwrap(); + let token_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + assert_eq!( + token_account.base.amount, + spl_token::ui_amount_to_amount(1.0, TEST_DECIMALS) + ); + + // transfer fails to non-system recipient without flag + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Transfer.into(), + "--fund-recipient", + &token.to_string(), + "1", + &system_program::id().to_string(), + ], + ) + .await + .unwrap_err(); + + // with non-system flag, transfer goes through + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Transfer.into(), + "--fund-recipient", + "--allow-non-system-account-recipient", + &token.to_string(), + "1", + &system_program::id().to_string(), + ], + ) + .await + .unwrap(); + let account = config + .rpc_client + .get_account(&system_token_account) + .await + .unwrap(); + let token_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + assert_eq!( + token_account.base.amount, + spl_token::ui_amount_to_amount(1.0, TEST_DECIMALS) + ); + + // transfer to same-program non-account fails + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Transfer.into(), + "--fund-recipient", + "--allow-non-system-account-recipient", + "--allow-unfunded-recipient", + &token.to_string(), + "1", + &token.to_string(), + ], + ) + .await + .unwrap_err(); + + // transfer to other-program account fails + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Transfer.into(), + "--fund-recipient", + "--allow-non-system-account-recipient", + "--allow-unfunded-recipient", + &token.to_string(), + "1", + &wrong_program_account.to_string(), + ], + ) + .await + .unwrap_err(); + } + } +} + +#[tokio::test] +#[serial] +async fn allow_non_system_account_recipient() { + let (test_validator, payer) = new_validator_for_test().await; + let config = test_config_with_default_signer(&test_validator, &payer, &spl_token::id()); + + let token = create_token(&config, &payer).await; + let source = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; + let recipient = Keypair::new().pubkey().to_string(); + let ui_amount = 100.0; + mint_tokens(&config, &payer, token, ui_amount, source) + .await + .unwrap(); + let result = process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Transfer.into(), + "--fund-recipient", + "--allow-non-system-account-recipient", + "--allow-unfunded-recipient", + &token.to_string(), + "10", + &recipient, + ], + ) + .await; + result.unwrap(); + + let ui_account = config + .rpc_client + .get_token_account(&source) + .await + .unwrap() + .unwrap(); + let amount = spl_token::ui_amount_to_amount(90.0, TEST_DECIMALS); + assert_eq!(ui_account.token_amount.amount, format!("{amount}")); +} + +#[tokio::test] +#[serial] +async fn close_account() { + let (test_validator, payer) = new_validator_for_test().await; + for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { + let config = test_config_with_default_signer(&test_validator, &payer, program_id); + + let native_mint = Token::new_native( + config.program_client.clone(), + program_id, + config.fee_payer().unwrap().clone(), + ); + do_create_native_mint(&config, program_id, &payer).await; + native_mint + .get_or_create_associated_account_info(&payer.pubkey()) + .await + .unwrap(); + + let token = create_token(&config, &payer).await; + + let system_recipient = Keypair::new().pubkey(); + let wsol_recipient = native_mint.get_associated_token_address(&payer.pubkey()); + + let token_rent_amount = config + .rpc_client + .get_account(&create_auxiliary_account(&config, &payer, token).await) + .await + .unwrap() + .lamports; + + for recipient in [system_recipient, wsol_recipient] { + let base_balance = config + .rpc_client + .get_account(&recipient) + .await + .map(|account| account.lamports) + .unwrap_or(0); + + let source = create_auxiliary_account(&config, &payer, token).await; + + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Close.into(), + "--address", + &source.to_string(), + "--recipient", + &recipient.to_string(), + ], + ) + .await + .unwrap(); + + let recipient_data = config.rpc_client.get_account(&recipient).await.unwrap(); + + assert_eq!(recipient_data.lamports, base_balance + token_rent_amount); + if recipient == wsol_recipient { + let recipient_account = + StateWithExtensionsOwned::::unpack(recipient_data.data).unwrap(); + assert_eq!(recipient_account.base.amount, token_rent_amount); + } + } + } +} + +#[tokio::test] +#[serial] +async fn close_wrapped_sol_account() { + let (test_validator, payer) = new_validator_for_test().await; + for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { + let config = test_config_with_default_signer(&test_validator, &payer, program_id); + + let native_mint = *Token::new_native( + config.program_client.clone(), + program_id, + config.fee_payer().unwrap().clone(), + ) + .get_address(); + let token = create_token(&config, &payer).await; + let source = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; + do_create_native_mint(&config, program_id, &payer).await; + let _result = process_test_command( + &config, + &payer, + &["spl-token", CommandName::Wrap.into(), "10.0"], + ) + .await + .unwrap(); + + let recipient = + get_associated_token_address_with_program_id(&payer.pubkey(), &native_mint, program_id); + let result = process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Close.into(), + "--address", + &source.to_string(), + "--recipient", + &recipient.to_string(), + ], + ) + .await; + result.unwrap(); + + let ui_account = config + .rpc_client + .get_token_account(&recipient) + .await + .unwrap() + .unwrap(); + assert_eq!(ui_account.token_amount.amount, "10000000000"); + } +} + +#[tokio::test] +#[serial] +async fn disable_mint_authority() { + let (test_validator, payer) = new_validator_for_test().await; + for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { + let config = test_config_with_default_signer(&test_validator, &payer, program_id); + let token = create_token(&config, &payer).await; + let result = process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Authorize.into(), + &token.to_string(), + "mint", + "--disable", + ], + ) + .await; + result.unwrap(); + + let account = config.rpc_client.get_account(&token).await.unwrap(); + let mint = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + assert_eq!(mint.base.mint_authority, COption::None); + } +} + +#[tokio::test] +#[serial] +async fn gc() { + let (test_validator, payer) = new_validator_for_test().await; + for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { + let mut config = test_config_with_default_signer(&test_validator, &payer, program_id); + let token = create_token(&config, &payer).await; + let _account = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; + let _aux1 = create_auxiliary_account(&config, &payer, token).await; + let _aux2 = create_auxiliary_account(&config, &payer, token).await; + let _aux3 = create_auxiliary_account(&config, &payer, token).await; + let result = process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Accounts.into(), + &token.to_string(), + ], + ) + .await + .unwrap(); + let value: serde_json::Value = serde_json::from_str(&result).unwrap(); + assert_eq!(value["accounts"].as_array().unwrap().len(), 4); + config.output_format = OutputFormat::Display; // fixup eventually? + let _result = process_test_command(&config, &payer, &["spl-token", CommandName::Gc.into()]) + .await + .unwrap(); + config.output_format = OutputFormat::JsonCompact; + let result = process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Accounts.into(), + &token.to_string(), + ], + ) + .await + .unwrap(); + let value: serde_json::Value = serde_json::from_str(&result).unwrap(); + assert_eq!(value["accounts"].as_array().unwrap().len(), 1); + + config.output_format = OutputFormat::Display; + + // test implicit transfer + let token = create_token(&config, &payer).await; + let ata = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; + let aux = create_auxiliary_account(&config, &payer, token).await; + mint_tokens(&config, &payer, token, 1.0, ata).await.unwrap(); + mint_tokens(&config, &payer, token, 1.0, aux).await.unwrap(); + + process_test_command(&config, &payer, &["spl-token", CommandName::Gc.into()]) + .await + .unwrap(); + + let ui_ata = config + .rpc_client + .get_token_account(&ata) + .await + .unwrap() + .unwrap(); + + // aux is gone and its tokens are in ata + let amount = spl_token::ui_amount_to_amount(2.0, TEST_DECIMALS); + assert_eq!(ui_ata.token_amount.amount, format!("{amount}")); + config.rpc_client.get_account(&aux).await.unwrap_err(); + + // test ata closure + let token = create_token(&config, &payer).await; + let ata = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; + + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Gc.into(), + "--close-empty-associated-accounts", + ], + ) + .await + .unwrap(); + + // ata is gone + config.rpc_client.get_account(&ata).await.unwrap_err(); + + // test a tricky corner case of both + let token = create_token(&config, &payer).await; + let ata = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; + let aux = create_auxiliary_account(&config, &payer, token).await; + mint_tokens(&config, &payer, token, 1.0, aux).await.unwrap(); + + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Gc.into(), + "--close-empty-associated-accounts", + ], + ) + .await + .unwrap(); + + let ui_ata = config + .rpc_client + .get_token_account(&ata) + .await + .unwrap() + .unwrap(); + + // aux is gone and its tokens are in ata, and ata has not been closed + let amount = spl_token::ui_amount_to_amount(1.0, TEST_DECIMALS); + assert_eq!(ui_ata.token_amount.amount, format!("{amount}")); + config.rpc_client.get_account(&aux).await.unwrap_err(); + + // test that balance moves off an uncloseable account + let token = create_token(&config, &payer).await; + let ata = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; + let aux = create_auxiliary_account(&config, &payer, token).await; + let close_authority = Keypair::new().pubkey(); + mint_tokens(&config, &payer, token, 1.0, aux).await.unwrap(); + + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Authorize.into(), + &aux.to_string(), + "close", + &close_authority.to_string(), + ], + ) + .await + .unwrap(); + + process_test_command(&config, &payer, &["spl-token", CommandName::Gc.into()]) + .await + .unwrap(); + + let ui_ata = config + .rpc_client + .get_token_account(&ata) + .await + .unwrap() + .unwrap(); + + // aux tokens are now in ata + let amount = spl_token::ui_amount_to_amount(1.0, TEST_DECIMALS); + assert_eq!(ui_ata.token_amount.amount, format!("{amount}")); + } +} + +#[tokio::test] +#[serial] +async fn set_owner() { + let (test_validator, payer) = new_validator_for_test().await; + for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { + let config = test_config_with_default_signer(&test_validator, &payer, program_id); + let token = create_token(&config, &payer).await; + let aux = create_auxiliary_account(&config, &payer, token).await; + let aux_string = aux.to_string(); + let _result = process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Authorize.into(), + &aux_string, + "owner", + &aux_string, + ], + ) + .await + .unwrap(); + let account = config.rpc_client.get_account(&aux).await.unwrap(); + let token_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + assert_eq!(token_account.base.mint, token); + assert_eq!(token_account.base.owner, aux); + } +} + +#[tokio::test] +#[serial] +async fn transfer_with_account_delegate() { + let (test_validator, payer) = new_validator_for_test().await; + for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { + let config = test_config_with_default_signer(&test_validator, &payer, program_id); + + let token = create_token(&config, &payer).await; + let source = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; + let destination = create_auxiliary_account(&config, &payer, token).await; + let delegate = Keypair::new(); + + let delegate_keypair_file = NamedTempFile::new().unwrap(); + write_keypair_file(&delegate, &delegate_keypair_file).unwrap(); + let fee_payer_keypair_file = NamedTempFile::new().unwrap(); + write_keypair_file(&payer, &fee_payer_keypair_file).unwrap(); + + let ui_amount = 100.0; + mint_tokens(&config, &payer, token, ui_amount, source) + .await + .unwrap(); + + let ui_account = config + .rpc_client + .get_token_account(&source) + .await + .unwrap() + .unwrap(); + let amount = spl_token::ui_amount_to_amount(100.0, TEST_DECIMALS); + assert_eq!(ui_account.token_amount.amount, format!("{amount}")); + assert_eq!(ui_account.delegate, None); + assert_eq!(ui_account.delegated_amount, None); + let ui_account = config + .rpc_client + .get_token_account(&destination) + .await + .unwrap() + .unwrap(); + assert_eq!(ui_account.token_amount.amount, "0"); + + exec_test_cmd( + &config, + &[ + "spl-token", + CommandName::Approve.into(), + &source.to_string(), + "10", + &delegate.pubkey().to_string(), + "--owner", + fee_payer_keypair_file.path().to_str().unwrap(), + "--fee-payer", + fee_payer_keypair_file.path().to_str().unwrap(), + "--program-id", + &program_id.to_string(), + ], + ) + .await + .unwrap(); + + let ui_account = config + .rpc_client + .get_token_account(&source) + .await + .unwrap() + .unwrap(); + assert_eq!(ui_account.delegate.unwrap(), delegate.pubkey().to_string()); + let amount = spl_token::ui_amount_to_amount(10.0, TEST_DECIMALS); + assert_eq!( + ui_account.delegated_amount.unwrap().amount, + format!("{amount}") + ); + + let result = exec_test_cmd( + &config, + &[ + "spl-token", + CommandName::Transfer.into(), + &token.to_string(), + "10", + &destination.to_string(), + "--from", + &source.to_string(), + "--owner", + delegate_keypair_file.path().to_str().unwrap(), + "--fee-payer", + fee_payer_keypair_file.path().to_str().unwrap(), + "--program-id", + &program_id.to_string(), + ], + ) + .await; + result.unwrap(); + + let ui_account = config + .rpc_client + .get_token_account(&source) + .await + .unwrap() + .unwrap(); + let amount = spl_token::ui_amount_to_amount(90.0, TEST_DECIMALS); + assert_eq!(ui_account.token_amount.amount, format!("{amount}")); + assert_eq!(ui_account.delegate, None); + assert_eq!(ui_account.delegated_amount, None); + let ui_account = config + .rpc_client + .get_token_account(&destination) + .await + .unwrap() + .unwrap(); + let amount = spl_token::ui_amount_to_amount(10.0, TEST_DECIMALS); + assert_eq!(ui_account.token_amount.amount, format!("{amount}")); + } +} + +#[tokio::test] +#[serial] +async fn burn_with_account_delegate() { + let (test_validator, payer) = new_validator_for_test().await; + for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { + let config = test_config_with_default_signer(&test_validator, &payer, program_id); + + let token = create_token(&config, &payer).await; + let source = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; + let delegate = Keypair::new(); + + let delegate_keypair_file = NamedTempFile::new().unwrap(); + write_keypair_file(&delegate, &delegate_keypair_file).unwrap(); + let fee_payer_keypair_file = NamedTempFile::new().unwrap(); + write_keypair_file(&payer, &fee_payer_keypair_file).unwrap(); + + let ui_amount = 100.0; + mint_tokens(&config, &payer, token, ui_amount, source) + .await + .unwrap(); + + let ui_account = config + .rpc_client + .get_token_account(&source) + .await + .unwrap() + .unwrap(); + let amount = spl_token::ui_amount_to_amount(100.0, TEST_DECIMALS); + assert_eq!(ui_account.token_amount.amount, format!("{amount}")); + assert_eq!(ui_account.delegate, None); + assert_eq!(ui_account.delegated_amount, None); + + exec_test_cmd( + &config, + &[ + "spl-token", + CommandName::Approve.into(), + &source.to_string(), + "10", + &delegate.pubkey().to_string(), + "--owner", + fee_payer_keypair_file.path().to_str().unwrap(), + "--fee-payer", + fee_payer_keypair_file.path().to_str().unwrap(), + "--program-id", + &program_id.to_string(), + ], + ) + .await + .unwrap(); + + let ui_account = config + .rpc_client + .get_token_account(&source) + .await + .unwrap() + .unwrap(); + assert_eq!(ui_account.delegate.unwrap(), delegate.pubkey().to_string()); + let amount = spl_token::ui_amount_to_amount(10.0, TEST_DECIMALS); + assert_eq!( + ui_account.delegated_amount.unwrap().amount, + format!("{amount}") + ); + + let result = exec_test_cmd( + &config, + &[ + "spl-token", + CommandName::Burn.into(), + &source.to_string(), + "10", + "--owner", + delegate_keypair_file.path().to_str().unwrap(), + "--fee-payer", + fee_payer_keypair_file.path().to_str().unwrap(), + "--program-id", + &program_id.to_string(), + ], + ) + .await; + result.unwrap(); + + let ui_account = config + .rpc_client + .get_token_account(&source) + .await + .unwrap() + .unwrap(); + let amount = spl_token::ui_amount_to_amount(90.0, TEST_DECIMALS); + assert_eq!(ui_account.token_amount.amount, format!("{amount}")); + assert_eq!(ui_account.delegate, None); + assert_eq!(ui_account.delegated_amount, None); + } +} + +#[tokio::test] +#[serial] +async fn close_mint() { + let (test_validator, payer) = new_validator_for_test().await; + let config = test_config_with_default_signer(&test_validator, &payer, &spl_token_2022::id()); + + let token = Keypair::new(); + let token_pubkey = token.pubkey(); + let token_keypair_file = NamedTempFile::new().unwrap(); + write_keypair_file(&token, &token_keypair_file).unwrap(); + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::CreateToken.into(), + token_keypair_file.path().to_str().unwrap(), + "--enable-close", + ], + ) + .await + .unwrap(); + + let account = config.rpc_client.get_account(&token_pubkey).await.unwrap(); + let test_mint = StateWithExtensionsOwned::::unpack(account.data); + assert!(test_mint.is_ok()); + + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::CloseMint.into(), + &token_pubkey.to_string(), + ], + ) + .await + .unwrap(); + + let account = config.rpc_client.get_account(&token_pubkey).await; + assert!(account.is_err()); +} + +#[tokio::test] +#[serial] +async fn burn_with_permanent_delegate() { + let (test_validator, payer) = new_validator_for_test().await; + let config = test_config_with_default_signer(&test_validator, &payer, &spl_token_2022::id()); + + let token = Keypair::new(); + let token_keypair_file = NamedTempFile::new().unwrap(); + write_keypair_file(&token, &token_keypair_file).unwrap(); + let token = token.pubkey(); + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::CreateToken.into(), + token_keypair_file.path().to_str().unwrap(), + "--enable-permanent-delegate", + ], + ) + .await + .unwrap(); + + let permanent_delegate_keypair_file = NamedTempFile::new().unwrap(); + write_keypair_file(&payer, &permanent_delegate_keypair_file).unwrap(); + + let unknown_owner = Keypair::new(); + let source = + create_associated_account(&config, &unknown_owner, &token, &unknown_owner.pubkey()).await; + let ui_amount = 100.0; + + mint_tokens(&config, &payer, token, ui_amount, source) + .await + .unwrap(); + + let ui_account = config + .rpc_client + .get_token_account(&source) + .await + .unwrap() + .unwrap(); + + let amount = spl_token::ui_amount_to_amount(100.0, TEST_DECIMALS); + assert_eq!(ui_account.token_amount.amount, format!("{amount}")); + + exec_test_cmd( + &config, + &[ + "spl-token", + CommandName::Burn.into(), + &source.to_string(), + "10", + "--owner", + permanent_delegate_keypair_file.path().to_str().unwrap(), + ], + ) + .await + .unwrap(); + + let ui_account = config + .rpc_client + .get_token_account(&source) + .await + .unwrap() + .unwrap(); + + let amount = spl_token::ui_amount_to_amount(90.0, TEST_DECIMALS); + assert_eq!(ui_account.token_amount.amount, format!("{amount}")); +} + +#[tokio::test] +#[serial] +async fn transfer_with_permanent_delegate() { + let (test_validator, payer) = new_validator_for_test().await; + let config = test_config_with_default_signer(&test_validator, &payer, &spl_token_2022::id()); + + let token = Keypair::new(); + let token_keypair_file = NamedTempFile::new().unwrap(); + write_keypair_file(&token, &token_keypair_file).unwrap(); + let token = token.pubkey(); + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::CreateToken.into(), + token_keypair_file.path().to_str().unwrap(), + "--enable-permanent-delegate", + ], + ) + .await + .unwrap(); + + let unknown_owner = Keypair::new(); + let source = + create_associated_account(&config, &unknown_owner, &token, &unknown_owner.pubkey()).await; + let destination = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; + + let permanent_delegate_keypair_file = NamedTempFile::new().unwrap(); + write_keypair_file(&payer, &permanent_delegate_keypair_file).unwrap(); + + let ui_amount = 100.0; + mint_tokens(&config, &payer, token, ui_amount, source) + .await + .unwrap(); + + let ui_account = config + .rpc_client + .get_token_account(&source) + .await + .unwrap() + .unwrap(); + + let amount = spl_token::ui_amount_to_amount(100.0, TEST_DECIMALS); + assert_eq!(ui_account.token_amount.amount, format!("{amount}")); + + let ui_account = config + .rpc_client + .get_token_account(&destination) + .await + .unwrap() + .unwrap(); + + assert_eq!(ui_account.token_amount.amount, "0"); + + exec_test_cmd( + &config, + &[ + "spl-token", + CommandName::Transfer.into(), + &token.to_string(), + "50", + &destination.to_string(), + "--from", + &source.to_string(), + "--owner", + permanent_delegate_keypair_file.path().to_str().unwrap(), + ], + ) + .await + .unwrap(); + + let ui_account = config + .rpc_client + .get_token_account(&destination) + .await + .unwrap() + .unwrap(); + + let amount = spl_token::ui_amount_to_amount(50.0, TEST_DECIMALS); + assert_eq!(ui_account.token_amount.amount, format!("{amount}")); + + let ui_account = config + .rpc_client + .get_token_account(&source) + .await + .unwrap() + .unwrap(); + + let amount = spl_token::ui_amount_to_amount(50.0, TEST_DECIMALS); + assert_eq!(ui_account.token_amount.amount, format!("{amount}")); +} + +#[tokio::test] +#[serial] +async fn required_transfer_memos() { + let (test_validator, payer) = new_validator_for_test().await; + let program_id = spl_token_2022::id(); + let config = test_config_with_default_signer(&test_validator, &payer, &program_id); + let token = create_token(&config, &payer).await; + let token_account = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; + let source_account = create_auxiliary_account(&config, &payer, token).await; + mint_tokens(&config, &payer, token, 100.0, source_account) + .await + .unwrap(); + + // enable works + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::EnableRequiredTransferMemos.into(), + &token_account.to_string(), + ], + ) + .await + .unwrap(); + let extensions = StateWithExtensionsOwned::::unpack( + config + .rpc_client + .get_account(&token_account) + .await + .unwrap() + .data, + ) + .unwrap(); + let memo_transfer = extensions.get_extension::().unwrap(); + let enabled: bool = memo_transfer.require_incoming_transfer_memos.into(); + assert!(enabled); + + // transfer requires a memo + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Transfer.into(), + "--from", + &source_account.to_string(), + &token.to_string(), + "1", + &token_account.to_string(), + ], + ) + .await + .unwrap_err(); + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Transfer.into(), + "--from", + &source_account.to_string(), + // malicious compliance + "--with-memo", + "memo", + &token.to_string(), + "1", + &token_account.to_string(), + ], + ) + .await + .unwrap(); + let account_data = config.rpc_client.get_account(&token_account).await.unwrap(); + let account_state = StateWithExtensionsOwned::::unpack(account_data.data).unwrap(); + assert_eq!( + account_state.base.amount, + spl_token::ui_amount_to_amount(1.0, TEST_DECIMALS) + ); + + // disable works + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::DisableRequiredTransferMemos.into(), + &token_account.to_string(), + ], + ) + .await + .unwrap(); + let extensions = StateWithExtensionsOwned::::unpack( + config + .rpc_client + .get_account(&token_account) + .await + .unwrap() + .data, + ) + .unwrap(); + let memo_transfer = extensions.get_extension::().unwrap(); + let enabled: bool = memo_transfer.require_incoming_transfer_memos.into(); + assert!(!enabled); +} + +#[tokio::test] +#[serial] +async fn cpi_guard() { + let (test_validator, payer) = new_validator_for_test().await; + let program_id = spl_token_2022::id(); + let config = test_config_with_default_signer(&test_validator, &payer, &program_id); + let token = create_token(&config, &payer).await; + let token_account = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; + + // enable works + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::EnableCpiGuard.into(), + &token_account.to_string(), + ], + ) + .await + .unwrap(); + let extensions = StateWithExtensionsOwned::::unpack( + config + .rpc_client + .get_account(&token_account) + .await + .unwrap() + .data, + ) + .unwrap(); + let cpi_guard = extensions.get_extension::().unwrap(); + let enabled: bool = cpi_guard.lock_cpi.into(); + assert!(enabled); + + // disable works + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::DisableCpiGuard.into(), + &token_account.to_string(), + ], + ) + .await + .unwrap(); + let extensions = StateWithExtensionsOwned::::unpack( + config + .rpc_client + .get_account(&token_account) + .await + .unwrap() + .data, + ) + .unwrap(); + let cpi_guard = extensions.get_extension::().unwrap(); + let enabled: bool = cpi_guard.lock_cpi.into(); + assert!(!enabled); +} + +#[tokio::test] +#[serial] +async fn immutable_accounts() { + let (test_validator, payer) = new_validator_for_test().await; + let program_id = spl_token_2022::id(); + let config = test_config_with_default_signer(&test_validator, &payer, &program_id); + let token = create_token(&config, &payer).await; + let new_owner = Keypair::new().pubkey(); + let native_mint = *Token::new_native( + config.program_client.clone(), + &program_id, + config.fee_payer().unwrap().clone(), + ) + .get_address(); + do_create_native_mint(&config, &program_id, &payer).await; + + // cannot reassign an ata + let account = create_associated_account(&config, &payer, &token, &payer.pubkey()).await; + let result = process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Authorize.into(), + &account.to_string(), + "owner", + &new_owner.to_string(), + ], + ) + .await; + result.unwrap_err(); + + // immutable works for create-account + let aux_account = Keypair::new(); + let aux_pubkey = aux_account.pubkey(); + let aux_keypair_file = NamedTempFile::new().unwrap(); + write_keypair_file(&aux_account, &aux_keypair_file).unwrap(); + + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::CreateAccount.into(), + &token.to_string(), + aux_keypair_file.path().to_str().unwrap(), + "--immutable", + ], + ) + .await + .unwrap(); + + let result = process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Authorize.into(), + &aux_pubkey.to_string(), + "owner", + &new_owner.to_string(), + ], + ) + .await; + result.unwrap_err(); + + // immutable works for wrap + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Wrap.into(), + "--create-aux-account", + "--immutable", + "0.5", + ], + ) + .await + .unwrap(); + + let accounts = config + .rpc_client + .get_token_accounts_by_owner(&payer.pubkey(), TokenAccountsFilter::Mint(native_mint)) + .await + .unwrap(); + + let result = process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Authorize.into(), + &accounts[0].pubkey, + "owner", + &new_owner.to_string(), + ], + ) + .await; + result.unwrap_err(); +} + +#[tokio::test] +#[serial] +async fn non_transferable() { + let (test_validator, payer) = new_validator_for_test().await; + let config = test_config_with_default_signer(&test_validator, &payer, &spl_token_2022::id()); + + let token = Keypair::new(); + let token_keypair_file = NamedTempFile::new().unwrap(); + write_keypair_file(&token, &token_keypair_file).unwrap(); + let token_pubkey = token.pubkey(); + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::CreateToken.into(), + token_keypair_file.path().to_str().unwrap(), + "--enable-non-transferable", + ], + ) + .await + .unwrap(); + + let account = config.rpc_client.get_account(&token_pubkey).await.unwrap(); + let test_mint = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + assert!(test_mint.get_extension::().is_ok()); + + let associated_account = + create_associated_account(&config, &payer, &token_pubkey, &payer.pubkey()).await; + let aux_account = create_auxiliary_account(&config, &payer, token_pubkey).await; + mint_tokens(&config, &payer, token_pubkey, 100.0, associated_account) + .await + .unwrap(); + + // transfer not allowed + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Transfer.into(), + "--from", + &associated_account.to_string(), + &token_pubkey.to_string(), + "1", + &aux_account.to_string(), + ], + ) + .await + .unwrap_err(); +} + +#[tokio::test] +#[serial] +async fn default_account_state() { + let (test_validator, payer) = new_validator_for_test().await; + let program_id = spl_token_2022::id(); + let config = test_config_with_default_signer(&test_validator, &payer, &program_id); + + let token = Keypair::new(); + let token_keypair_file = NamedTempFile::new().unwrap(); + write_keypair_file(&token, &token_keypair_file).unwrap(); + let token_pubkey = token.pubkey(); + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::CreateToken.into(), + token_keypair_file.path().to_str().unwrap(), + "--enable-freeze", + "--default-account-state", + "frozen", + ], + ) + .await + .unwrap(); + + let mint_account = config.rpc_client.get_account(&token_pubkey).await.unwrap(); + let mint = StateWithExtensionsOwned::::unpack(mint_account.data).unwrap(); + let extension = mint.get_extension::().unwrap(); + assert_eq!(extension.state, u8::from(AccountState::Frozen)); + + let frozen_account = + create_associated_account(&config, &payer, &token_pubkey, &payer.pubkey()).await; + let token_account = config + .rpc_client + .get_account(&frozen_account) + .await + .unwrap(); + let account = StateWithExtensionsOwned::::unpack(token_account.data).unwrap(); + assert_eq!(account.base.state, AccountState::Frozen); + + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::UpdateDefaultAccountState.into(), + &token_pubkey.to_string(), + "initialized", + ], + ) + .await + .unwrap(); + let unfrozen_account = create_auxiliary_account(&config, &payer, token_pubkey).await; + let token_account = config + .rpc_client + .get_account(&unfrozen_account) + .await + .unwrap(); + let account = StateWithExtensionsOwned::::unpack(token_account.data).unwrap(); + assert_eq!(account.base.state, AccountState::Initialized); +} + +#[tokio::test] +#[serial] +async fn transfer_fee() { + let (test_validator, payer) = new_validator_for_test().await; + let config = test_config_with_default_signer(&test_validator, &payer, &spl_token_2022::id()); + + let transfer_fee_basis_points = 100; + let maximum_fee = 10_000_000_000; + + let token = Keypair::new(); + let token_keypair_file = NamedTempFile::new().unwrap(); + write_keypair_file(&token, &token_keypair_file).unwrap(); + let token_pubkey = token.pubkey(); + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::CreateToken.into(), + token_keypair_file.path().to_str().unwrap(), + "--transfer-fee", + &transfer_fee_basis_points.to_string(), + &maximum_fee.to_string(), + ], + ) + .await + .unwrap(); + + let account = config.rpc_client.get_account(&token_pubkey).await.unwrap(); + let test_mint = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + let extension = test_mint.get_extension::().unwrap(); + assert_eq!( + u16::from(extension.older_transfer_fee.transfer_fee_basis_points), + transfer_fee_basis_points + ); + assert_eq!( + u64::from(extension.older_transfer_fee.maximum_fee), + maximum_fee + ); + assert_eq!( + u16::from(extension.newer_transfer_fee.transfer_fee_basis_points), + transfer_fee_basis_points + ); + assert_eq!( + u64::from(extension.newer_transfer_fee.maximum_fee), + maximum_fee + ); + + let total_amount = 1000.0; + let transfer_amount = 100.0; + let token_account = + create_associated_account(&config, &payer, &token_pubkey, &payer.pubkey()).await; + let source_account = create_auxiliary_account(&config, &payer, token_pubkey).await; + mint_tokens(&config, &payer, token_pubkey, total_amount, source_account) + .await + .unwrap(); + + // withdraw from account directly + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Transfer.into(), + "--from", + &source_account.to_string(), + &token_pubkey.to_string(), + &transfer_amount.to_string(), + &token_account.to_string(), + "--expected-fee", + "1", + ], + ) + .await + .unwrap(); + + let account = config.rpc_client.get_account(&token_account).await.unwrap(); + let account_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + let extension = account_state.get_extension::().unwrap(); + let withheld_amount = + spl_token::amount_to_ui_amount(u64::from(extension.withheld_amount), TEST_DECIMALS); + assert_eq!(withheld_amount, 1.0); + + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::WithdrawWithheldTokens.into(), + &token_account.to_string(), + &token_account.to_string(), + ], + ) + .await + .unwrap(); + + let account = config.rpc_client.get_account(&token_account).await.unwrap(); + let account_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + let extension = account_state.get_extension::().unwrap(); + assert_eq!(u64::from(extension.withheld_amount), 0); + + // withdraw from mint after account closure + // gather fees + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Transfer.into(), + "--from", + &source_account.to_string(), + &token_pubkey.to_string(), + &(total_amount - transfer_amount).to_string(), + &token_account.to_string(), + "--expected-fee", + "9", + ], + ) + .await + .unwrap(); + + // burn tokens + let account = config.rpc_client.get_account(&token_account).await.unwrap(); + let account_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + let burn_amount = spl_token::amount_to_ui_amount(account_state.base.amount, TEST_DECIMALS); + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Burn.into(), + &token_account.to_string(), + &burn_amount.to_string(), + ], + ) + .await + .unwrap(); + + let account = config.rpc_client.get_account(&token_account).await.unwrap(); + let account_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + let extension = account_state.get_extension::().unwrap(); + let withheld_amount = + spl_token::amount_to_ui_amount(u64::from(extension.withheld_amount), TEST_DECIMALS); + assert_eq!(withheld_amount, 9.0); + + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Close.into(), + "--address", + &token_account.to_string(), + "--recipient", + &payer.pubkey().to_string(), + ], + ) + .await + .unwrap(); + + let mint = config.rpc_client.get_account(&token_pubkey).await.unwrap(); + let mint_state = StateWithExtensionsOwned::::unpack(mint.data).unwrap(); + let extension = mint_state.get_extension::().unwrap(); + let withheld_amount = + spl_token::amount_to_ui_amount(u64::from(extension.withheld_amount), TEST_DECIMALS); + assert_eq!(withheld_amount, 9.0); + + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::WithdrawWithheldTokens.into(), + &source_account.to_string(), + "--include-mint", + ], + ) + .await + .unwrap(); + + let mint = config.rpc_client.get_account(&token_pubkey).await.unwrap(); + let mint_state = StateWithExtensionsOwned::::unpack(mint.data).unwrap(); + let extension = mint_state.get_extension::().unwrap(); + assert_eq!(u64::from(extension.withheld_amount), 0); + + // set the transfer fee + let new_transfer_fee_basis_points = 800; + let new_maximum_fee = 5_000_000.0; + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::SetTransferFee.into(), + &token_pubkey.to_string(), + &new_transfer_fee_basis_points.to_string(), + &new_maximum_fee.to_string(), + ], + ) + .await + .unwrap(); + + let mint = config.rpc_client.get_account(&token_pubkey).await.unwrap(); + let mint_state = StateWithExtensionsOwned::::unpack(mint.data).unwrap(); + let extension = mint_state.get_extension::().unwrap(); + assert_eq!( + u16::from(extension.newer_transfer_fee.transfer_fee_basis_points), + new_transfer_fee_basis_points + ); + let new_maximum_fee = spl_token::ui_amount_to_amount(new_maximum_fee, TEST_DECIMALS); + assert_eq!( + u64::from(extension.newer_transfer_fee.maximum_fee), + new_maximum_fee + ); + + // disable transfer fee authority + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Authorize.into(), + "--disable", + &token_pubkey.to_string(), + "transfer-fee-config", + ], + ) + .await + .unwrap(); + + let mint = config.rpc_client.get_account(&token_pubkey).await.unwrap(); + let mint_state = StateWithExtensionsOwned::::unpack(mint.data).unwrap(); + let extension = mint_state.get_extension::().unwrap(); + + assert_eq!( + Option::::try_from(extension.transfer_fee_config_authority).unwrap(), + None, + ); + + // disable withdraw withheld authority + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Authorize.into(), + "--disable", + &token_pubkey.to_string(), + "withheld-withdraw", + ], + ) + .await + .unwrap(); + + let mint = config.rpc_client.get_account(&token_pubkey).await.unwrap(); + let mint_state = StateWithExtensionsOwned::::unpack(mint.data).unwrap(); + let extension = mint_state.get_extension::().unwrap(); + + assert_eq!( + Option::::try_from(extension.withdraw_withheld_authority).unwrap(), + None, + ); +} + +#[tokio::test] +#[serial] +async fn confidential_transfer() { + use spl_token_2022::solana_zk_token_sdk::encryption::elgamal::ElGamalKeypair; + + let (test_validator, payer) = new_validator_for_test().await; + let config = test_config_with_default_signer(&test_validator, &payer, &spl_token_2022::id()); + + // create token with confidential transfers enabled + let auto_approve = false; + let confidential_transfer_mint_authority = payer.pubkey(); + + let token = Keypair::new(); + let token_keypair_file = NamedTempFile::new().unwrap(); + write_keypair_file(&token, &token_keypair_file).unwrap(); + let token_pubkey = token.pubkey(); + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::CreateToken.into(), + token_keypair_file.path().to_str().unwrap(), + "--enable-confidential-transfers", + "manual", + ], + ) + .await + .unwrap(); + + let account = config.rpc_client.get_account(&token_pubkey).await.unwrap(); + let test_mint = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + let extension = test_mint + .get_extension::() + .unwrap(); + + assert_eq!( + Option::::from(extension.authority), + Some(confidential_transfer_mint_authority), + ); + assert_eq!( + bool::from(extension.auto_approve_new_accounts), + auto_approve, + ); + assert_eq!( + Option::::from(extension.auditor_elgamal_pubkey), + None, + ); + + // update confidential transfer mint settings + let auditor_keypair = ElGamalKeypair::new_rand(); + let auditor_pubkey: ElGamalPubkey = (*auditor_keypair.pubkey()).into(); + let new_auto_approve = true; + + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::UpdateConfidentialTransferSettings.into(), + &token_pubkey.to_string(), + "--auditor-pubkey", + &auditor_pubkey.to_string(), + "--approve-policy", + "auto", + ], + ) + .await + .unwrap(); + + let account = config.rpc_client.get_account(&token_pubkey).await.unwrap(); + let test_mint = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + let extension = test_mint + .get_extension::() + .unwrap(); + + assert_eq!( + bool::from(extension.auto_approve_new_accounts), + new_auto_approve, + ); + assert_eq!( + Option::::from(extension.auditor_elgamal_pubkey), + Some(auditor_pubkey), + ); + + // create a confidential transfer account + let token_account = + create_associated_account(&config, &payer, &token_pubkey, &payer.pubkey()).await; + + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::ConfigureConfidentialTransferAccount.into(), + &token_pubkey.to_string(), + ], + ) + .await + .unwrap(); + + let account = config.rpc_client.get_account(&token_account).await.unwrap(); + let account_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + let extension = account_state + .get_extension::() + .unwrap(); + assert!(bool::from(extension.approved)); + assert!(bool::from(extension.allow_confidential_credits)); + assert!(bool::from(extension.allow_non_confidential_credits)); + + // disable and enable confidential transfers for an account + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::DisableConfidentialCredits.into(), + &token_pubkey.to_string(), + ], + ) + .await + .unwrap(); + + let account = config.rpc_client.get_account(&token_account).await.unwrap(); + let account_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + let extension = account_state + .get_extension::() + .unwrap(); + assert!(!bool::from(extension.allow_confidential_credits)); + + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::EnableConfidentialCredits.into(), + &token_pubkey.to_string(), + ], + ) + .await + .unwrap(); + + let account = config.rpc_client.get_account(&token_account).await.unwrap(); + let account_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + let extension = account_state + .get_extension::() + .unwrap(); + assert!(bool::from(extension.allow_confidential_credits)); + + // disable and eanble non-confidential transfers for an account + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::DisableNonConfidentialCredits.into(), + &token_pubkey.to_string(), + ], + ) + .await + .unwrap(); + + let account = config.rpc_client.get_account(&token_account).await.unwrap(); + let account_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + let extension = account_state + .get_extension::() + .unwrap(); + assert!(!bool::from(extension.allow_non_confidential_credits)); + + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::EnableNonConfidentialCredits.into(), + &token_pubkey.to_string(), + ], + ) + .await + .unwrap(); + + let account = config.rpc_client.get_account(&token_account).await.unwrap(); + let account_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + let extension = account_state + .get_extension::() + .unwrap(); + assert!(bool::from(extension.allow_non_confidential_credits)); + + // deposit confidential tokens + let deposit_amount = 100.0; + mint_tokens(&config, &payer, token_pubkey, deposit_amount, token_account) + .await + .unwrap(); + + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::DepositConfidentialTokens.into(), + &token_pubkey.to_string(), + &deposit_amount.to_string(), + ], + ) + .await + .unwrap(); + + // apply pending balance + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::ApplyPendingBalance.into(), + &token_pubkey.to_string(), + ], + ) + .await + .unwrap(); + + // confidential transfer + let destination_account = create_auxiliary_account(&config, &payer, token_pubkey).await; + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::ConfigureConfidentialTransferAccount.into(), + "--address", + &destination_account.to_string(), + ], + ) + .await + .unwrap(); // configure destination account for confidential transfers first + + let transfer_amount = 100.0; + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Transfer.into(), + &token_pubkey.to_string(), + &transfer_amount.to_string(), + &destination_account.to_string(), + "--confidential", + ], + ) + .await + .unwrap(); + + // withdraw confidential tokens + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::ApplyPendingBalance.into(), + "--address", + &destination_account.to_string(), + ], + ) + .await + .unwrap(); // apply pending balance first + + let withdraw_amount = 100.0; + + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::WithdrawConfidentialTokens.into(), + &token_pubkey.to_string(), + &withdraw_amount.to_string(), + "--address", + &destination_account.to_string(), + ], + ) + .await + .unwrap(); + + // disable confidential transfers for mint + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Authorize.into(), + &token_pubkey.to_string(), + "confidential-transfer-mint", + "--disable", + ], + ) + .await + .unwrap(); +} + +#[tokio::test] +#[serial] +async fn multisig_transfer() { + let (test_validator, payer) = new_validator_for_test().await; + let m = 3; + let n = 5u8; + // need to add "payer" to make the config provide the right signer + let (multisig_members, multisig_paths): (Vec<_>, Vec<_>) = + std::iter::once(clone_keypair(&payer)) + .chain(std::iter::repeat_with(Keypair::new).take((n - 2) as usize)) + .map(|s| { + let keypair_file = NamedTempFile::new().unwrap(); + write_keypair_file(&s, &keypair_file).unwrap(); + (s.pubkey(), keypair_file) + }) + .unzip(); + for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { + let config = test_config_with_default_signer(&test_validator, &payer, program_id); + let token = create_token(&config, &payer).await; + let multisig = Arc::new(Keypair::new()); + let multisig_pubkey = multisig.pubkey(); + + // add the multisig as a member to itself, make it self-owned + let multisig_members = std::iter::once(multisig_pubkey) + .chain(multisig_members.iter().cloned()) + .collect::>(); + let multisig_path = NamedTempFile::new().unwrap(); + write_keypair_file(&multisig, &multisig_path).unwrap(); + let multisig_paths = std::iter::once(&multisig_path) + .chain(multisig_paths.iter()) + .collect::>(); + + let multisig_strings = multisig_members + .iter() + .map(|p| p.to_string()) + .collect::>(); + process_test_command( + &config, + &payer, + [ + "spl-token", + CommandName::CreateMultisig.into(), + "--address-keypair", + multisig_path.path().to_str().unwrap(), + "--program-id", + &program_id.to_string(), + &m.to_string(), + ] + .into_iter() + .chain(multisig_strings.iter().map(|p| p.as_str())), + ) + .await + .unwrap(); + + let account = config + .rpc_client + .get_account(&multisig_pubkey) + .await + .unwrap(); + let multisig = Multisig::unpack(&account.data).unwrap(); + assert_eq!(multisig.m, m); + assert_eq!(multisig.n, n); + + let source = create_associated_account(&config, &payer, &token, &multisig_pubkey).await; + let destination = create_auxiliary_account(&config, &payer, token).await; + let ui_amount = 100.0; + mint_tokens(&config, &payer, token, ui_amount, source) + .await + .unwrap(); + + exec_test_cmd( + &config, + &[ + "spl-token", + CommandName::Transfer.into(), + &token.to_string(), + "10", + &destination.to_string(), + "--multisig-signer", + multisig_paths[0].path().to_str().unwrap(), + "--multisig-signer", + multisig_paths[1].path().to_str().unwrap(), + "--multisig-signer", + multisig_paths[2].path().to_str().unwrap(), + "--from", + &source.to_string(), + "--owner", + &multisig_pubkey.to_string(), + "--fee-payer", + multisig_paths[1].path().to_str().unwrap(), + ], + ) + .await + .unwrap(); + + let account = config.rpc_client.get_account(&source).await.unwrap(); + let token_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + assert_eq!( + token_account.base.amount, + spl_token::ui_amount_to_amount(90.0, TEST_DECIMALS) + ); + let account = config.rpc_client.get_account(&destination).await.unwrap(); + let token_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + assert_eq!( + token_account.base.amount, + spl_token::ui_amount_to_amount(10.0, TEST_DECIMALS) + ); + } +} + +#[tokio::test] +#[serial] +async fn offline_multisig_transfer_with_nonce() { + let (test_validator, payer) = new_validator_for_test().await; + let m = 2; + let n = 3u8; + + let (multisig_members, multisig_paths): (Vec<_>, Vec<_>) = std::iter::repeat_with(Keypair::new) + .take(n as usize) + .map(|s| { + let keypair_file = NamedTempFile::new().unwrap(); + write_keypair_file(&s, &keypair_file).unwrap(); + (s.pubkey(), keypair_file) + }) + .unzip(); + for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { + let mut config = test_config_with_default_signer(&test_validator, &payer, program_id); + let token = create_token(&config, &payer).await; + let nonce = create_nonce(&config, &payer).await; + + let nonce_account = config.rpc_client.get_account(&nonce).await.unwrap(); + let start_hash_index = 4 + 4 + 32; + let blockhash = Hash::new(&nonce_account.data[start_hash_index..start_hash_index + 32]); + + let multisig = Arc::new(Keypair::new()); + let multisig_pubkey = multisig.pubkey(); + let multisig_path = NamedTempFile::new().unwrap(); + write_keypair_file(&multisig, &multisig_path).unwrap(); + + let multisig_strings = multisig_members + .iter() + .map(|p| p.to_string()) + .collect::>(); + process_test_command( + &config, + &payer, + [ + "spl-token", + CommandName::CreateMultisig.into(), + "--address-keypair", + multisig_path.path().to_str().unwrap(), + "--program-id", + &program_id.to_string(), + &m.to_string(), + ] + .into_iter() + .chain(multisig_strings.iter().map(|p| p.as_str())), + ) + .await + .unwrap(); + + let source = create_associated_account(&config, &payer, &token, &multisig_pubkey).await; + let destination = create_auxiliary_account(&config, &payer, token).await; + let ui_amount = 100.0; + mint_tokens(&config, &payer, token, ui_amount, source) + .await + .unwrap(); + + let program_client: Arc> = Arc::new( + ProgramOfflineClient::new(blockhash, ProgramRpcClientSendTransaction), + ); + config.program_client = program_client; + let result = exec_test_cmd( + &config, + &[ + "spl-token", + CommandName::Transfer.into(), + &token.to_string(), + "10", + &destination.to_string(), + "--blockhash", + &blockhash.to_string(), + "--nonce", + &nonce.to_string(), + "--nonce-authority", + &payer.pubkey().to_string(), + "--sign-only", + "--mint-decimals", + &format!("{}", TEST_DECIMALS), + "--multisig-signer", + multisig_paths[1].path().to_str().unwrap(), + "--multisig-signer", + &multisig_members[2].to_string(), + "--from", + &source.to_string(), + "--owner", + &multisig_pubkey.to_string(), + "--fee-payer", + &multisig_members[0].to_string(), + ], + ) + .await + .unwrap(); + // the provided signer has a signature, denoted by the pubkey followed + // by "=" and the signature + assert!(result.contains(&format!("{}=", multisig_members[1]))); + + // other three expected signers are absent + let absent_signers_position = result.find("Absent Signers").unwrap(); + let absent_signers = result.get(absent_signers_position..).unwrap(); + assert!(absent_signers.contains(&multisig_members[0].to_string())); + assert!(absent_signers.contains(&multisig_members[2].to_string())); + assert!(absent_signers.contains(&payer.pubkey().to_string())); + + // and nothing else is marked a signer + assert!(!absent_signers.contains(&multisig_pubkey.to_string())); + assert!(!absent_signers.contains(&nonce.to_string())); + assert!(!absent_signers.contains(&source.to_string())); + assert!(!absent_signers.contains(&destination.to_string())); + assert!(!absent_signers.contains(&token.to_string())); + } +} + +#[tokio::test] +#[serial] +async fn withdraw_excess_lamports_from_multisig() { + let (test_validator, payer) = new_validator_for_test().await; + let m = 3; + let n = 5u8; + // need to add "payer" to make the config provide the right signer + let (multisig_members, multisig_paths): (Vec<_>, Vec<_>) = + std::iter::once(clone_keypair(&payer)) + .chain(std::iter::repeat_with(Keypair::new).take((n - 2) as usize)) + .map(|s| { + let keypair_file = NamedTempFile::new().unwrap(); + write_keypair_file(&s, &keypair_file).unwrap(); + (s.pubkey(), keypair_file) + }) + .unzip(); + + let fee_payer_keypair_file = NamedTempFile::new().unwrap(); + write_keypair_file(&payer, &fee_payer_keypair_file).unwrap(); + + let owner_keypair_file = NamedTempFile::new().unwrap(); + write_keypair_file(&payer, &owner_keypair_file).unwrap(); + + let program_id = &spl_token_2022::id(); + let config = test_config_with_default_signer(&test_validator, &payer, program_id); + + let multisig = Arc::new(Keypair::new()); + let multisig_pubkey = multisig.pubkey(); + + // add the multisig as a member to itself, make it self-owned + let multisig_members = std::iter::once(multisig_pubkey) + .chain(multisig_members.iter().cloned()) + .collect::>(); + let multisig_path = NamedTempFile::new().unwrap(); + write_keypair_file(&multisig, &multisig_path).unwrap(); + let multisig_paths = std::iter::once(&multisig_path) + .chain(multisig_paths.iter()) + .collect::>(); + + let multisig_strings = multisig_members + .iter() + .map(|p| p.to_string()) + .collect::>(); + process_test_command( + &config, + &payer, + [ + "spl-token", + CommandName::CreateMultisig.into(), + "--address-keypair", + multisig_path.path().to_str().unwrap(), + "--program-id", + &program_id.to_string(), + &m.to_string(), + ] + .into_iter() + .chain(multisig_strings.iter().map(|p| p.as_str())), + ) + .await + .unwrap(); + + let account = config + .rpc_client + .get_account(&multisig_pubkey) + .await + .unwrap(); + let multisig = Multisig::unpack(&account.data).unwrap(); + assert_eq!(multisig.m, m); + assert_eq!(multisig.n, n); + + let receiver = Keypair::new(); + let excess_lamports = 4000 * 1_000_000_000; + + config + .rpc_client + .send_and_confirm_transaction(&Transaction::new_signed_with_payer( + &[system_instruction::transfer( + &payer.pubkey(), + &multisig_pubkey, + excess_lamports, + )], + Some(&payer.pubkey()), + &[&payer], + config.rpc_client.get_latest_blockhash().await.unwrap(), + )) + .await + .unwrap(); + + exec_test_cmd( + &config, + &[ + "spl-token", + CommandName::WithdrawExcessLamports.into(), + &multisig_pubkey.to_string(), + &receiver.pubkey().to_string(), + "--owner", + &multisig_pubkey.to_string(), + "--multisig-signer", + multisig_paths[0].path().to_str().unwrap(), + "--multisig-signer", + multisig_paths[1].path().to_str().unwrap(), + "--multisig-signer", + multisig_paths[2].path().to_str().unwrap(), + "--fee-payer", + fee_payer_keypair_file.path().to_str().unwrap(), + "--program-id", + &program_id.to_string(), + ], + ) + .await + .unwrap(); + + assert_eq!( + excess_lamports, + config + .rpc_client + .get_balance(&receiver.pubkey()) + .await + .unwrap() + ); +} + +#[tokio::test] +#[serial] +async fn withdraw_excess_lamports_from_mint() { + let (test_validator, payer) = new_validator_for_test().await; + let program_id = &spl_token_2022::id(); + let config = test_config_with_default_signer(&test_validator, &payer, program_id); + let owner_keypair_file = NamedTempFile::new().unwrap(); + write_keypair_file(&payer, &owner_keypair_file).unwrap(); + + let receiver = Keypair::new(); + + let token_keypair = Keypair::new(); + let token_path = NamedTempFile::new().unwrap(); + write_keypair_file(&token_keypair, &token_path).unwrap(); + let token_pubkey = token_keypair.pubkey(); + + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::CreateToken.into(), + token_path.path().to_str().unwrap(), + "--program-id", + &program_id.to_string(), + ], + ) + .await + .unwrap(); + + let excess_lamports = 4000 * 1_000_000_000; + config + .rpc_client + .send_and_confirm_transaction(&Transaction::new_signed_with_payer( + &[system_instruction::transfer( + &payer.pubkey(), + &token_pubkey, + excess_lamports, + )], + Some(&payer.pubkey()), + &[&payer], + config.rpc_client.get_latest_blockhash().await.unwrap(), + )) + .await + .unwrap(); + + exec_test_cmd( + &config, + &[ + "spl-token", + CommandName::WithdrawExcessLamports.into(), + &token_pubkey.to_string(), + &receiver.pubkey().to_string(), + "--owner", + owner_keypair_file.path().to_str().unwrap(), + "--program-id", + &program_id.to_string(), + ], + ) + .await + .unwrap(); + + assert_eq!( + excess_lamports, + config + .rpc_client + .get_balance(&receiver.pubkey()) + .await + .unwrap() + ); +} + +#[tokio::test] +#[serial] +async fn withdraw_excess_lamports_from_account() { + let (test_validator, payer) = new_validator_for_test().await; + let program_id = &spl_token_2022::id(); + let config = test_config_with_default_signer(&test_validator, &payer, program_id); + let owner_keypair_file = NamedTempFile::new().unwrap(); + write_keypair_file(&payer, &owner_keypair_file).unwrap(); + + let receiver = Keypair::new(); + + let token_keypair = Keypair::new(); + let token_path = NamedTempFile::new().unwrap(); + write_keypair_file(&token_keypair, &token_path).unwrap(); + let token_pubkey = token_keypair.pubkey(); + + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::CreateToken.into(), + token_path.path().to_str().unwrap(), + "--program-id", + &program_id.to_string(), + ], + ) + .await + .unwrap(); + + let excess_lamports = 4000 * 1_000_000_000; + let token_account = + create_associated_account(&config, &payer, &token_pubkey, &payer.pubkey()).await; + + config + .rpc_client + .send_and_confirm_transaction(&Transaction::new_signed_with_payer( + &[system_instruction::transfer( + &payer.pubkey(), + &token_account, + excess_lamports, + )], + Some(&payer.pubkey()), + &[&payer], + config.rpc_client.get_latest_blockhash().await.unwrap(), + )) + .await + .unwrap(); + + exec_test_cmd( + &config, + &[ + "spl-token", + CommandName::WithdrawExcessLamports.into(), + &token_account.to_string(), + &receiver.pubkey().to_string(), + "--owner", + owner_keypair_file.path().to_str().unwrap(), + "--program-id", + &program_id.to_string(), + ], + ) + .await + .unwrap(); + + assert_eq!( + excess_lamports, + config + .rpc_client + .get_balance(&receiver.pubkey()) + .await + .unwrap() + ); +} + +#[tokio::test] +#[serial] +async fn metadata_pointer() { + let (test_validator, payer) = new_validator_for_test().await; + let program_id = spl_token_2022::id(); + let config = test_config_with_default_signer(&test_validator, &payer, &program_id); + let metadata_address = Pubkey::new_unique(); + + let result = process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::CreateToken.into(), + "--program-id", + &program_id.to_string(), + "--metadata-address", + &metadata_address.to_string(), + ], + ) + .await; + + let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); + let mint = Pubkey::from_str(value["commandOutput"]["address"].as_str().unwrap()).unwrap(); + let account = config.rpc_client.get_account(&mint).await.unwrap(); + let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + + let extension = mint_state.get_extension::().unwrap(); + + assert_eq!( + extension.metadata_address, + Some(metadata_address).try_into().unwrap() + ); + + let new_metadata_address = Pubkey::new_unique(); + + let _new_result = process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::UpdateMetadataAddress.into(), + &mint.to_string(), + &new_metadata_address.to_string(), + ], + ) + .await; + + let new_account = config.rpc_client.get_account(&mint).await.unwrap(); + let new_mint_state = StateWithExtensionsOwned::::unpack(new_account.data).unwrap(); + + let new_extension = new_mint_state.get_extension::().unwrap(); + + assert_eq!( + new_extension.metadata_address, + Some(new_metadata_address).try_into().unwrap() + ); + + let _result_with_disable = process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::UpdateMetadataAddress.into(), + &mint.to_string(), + "--disable", + ], + ) + .await; + + let new_account_disbale = config.rpc_client.get_account(&mint).await.unwrap(); + let new_mint_state_disable = + StateWithExtensionsOwned::::unpack(new_account_disbale.data).unwrap(); + + let new_extension_disable = new_mint_state_disable + .get_extension::() + .unwrap(); + + assert_eq!( + new_extension_disable.metadata_address, + None.try_into().unwrap() + ); +} + +#[tokio::test] +#[serial] +async fn transfer_hook() { + let (test_validator, payer) = new_validator_for_test().await; + let program_id = spl_token_2022::id(); + let mut config = test_config_with_default_signer(&test_validator, &payer, &program_id); + let transfer_hook_program_id = Pubkey::new_unique(); + + let result = process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::CreateToken.into(), + "--program-id", + &program_id.to_string(), + "--transfer-hook", + &transfer_hook_program_id.to_string(), + ], + ) + .await; + + let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); + let mint = Pubkey::from_str(value["commandOutput"]["address"].as_str().unwrap()).unwrap(); + let account = config.rpc_client.get_account(&mint).await.unwrap(); + let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + let extension = mint_state.get_extension::().unwrap(); + + assert_eq!( + extension.program_id, + Some(transfer_hook_program_id).try_into().unwrap() + ); + + let new_transfer_hook_program_id = Pubkey::new_unique(); + + let _new_result = process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::SetTransferHook.into(), + &mint.to_string(), + &new_transfer_hook_program_id.to_string(), + ], + ) + .await + .unwrap(); + + let account = config.rpc_client.get_account(&mint).await.unwrap(); + let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + let extension = mint_state.get_extension::().unwrap(); + + assert_eq!( + extension.program_id, + Some(new_transfer_hook_program_id).try_into().unwrap() + ); + + // Make sure that parsing transfer hook accounts works + let real_program_client = config.program_client; + let blockhash = Hash::default(); + let program_client: Arc> = Arc::new( + ProgramOfflineClient::new(blockhash, ProgramRpcClientSendTransaction), + ); + config.program_client = program_client; + let _result = exec_test_cmd( + &config, + &[ + "spl-token", + CommandName::Transfer.into(), + &mint.to_string(), + "10", + &Pubkey::new_unique().to_string(), + "--blockhash", + &blockhash.to_string(), + "--nonce", + &Pubkey::new_unique().to_string(), + "--nonce-authority", + &Pubkey::new_unique().to_string(), + "--sign-only", + "--mint-decimals", + &format!("{}", TEST_DECIMALS), + "--from", + &Pubkey::new_unique().to_string(), + "--owner", + &Pubkey::new_unique().to_string(), + "--transfer-hook-account", + &format!("{}:readonly", Pubkey::new_unique()), + "--transfer-hook-account", + &format!("{}:writable", Pubkey::new_unique()), + "--transfer-hook-account", + &format!("{}:readonly-signer", Pubkey::new_unique()), + "--transfer-hook-account", + &format!("{}:writable-signer", Pubkey::new_unique()), + ], + ) + .await + .unwrap(); + + config.program_client = real_program_client; + let _result_with_disable = process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::SetTransferHook.into(), + &mint.to_string(), + "--disable", + ], + ) + .await + .unwrap(); + + let account = config.rpc_client.get_account(&mint).await.unwrap(); + let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + let extension = mint_state.get_extension::().unwrap(); + + assert_eq!(extension.program_id, None.try_into().unwrap()); +} + +#[tokio::test] +#[serial] +async fn metadata() { + let (test_validator, payer) = new_validator_for_test().await; + let program_id = spl_token_2022::id(); + let config = test_config_with_default_signer(&test_validator, &payer, &program_id); + let name = "this"; + let symbol = "is"; + let uri = "METADATA!"; + + let result = process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::CreateToken.into(), + "--program-id", + &program_id.to_string(), + "--enable-metadata", + ], + ) + .await; + + let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); + let mint = Pubkey::from_str(value["commandOutput"]["address"].as_str().unwrap()).unwrap(); + let account = config.rpc_client.get_account(&mint).await.unwrap(); + let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + + let extension = mint_state.get_extension::().unwrap(); + assert_eq!(extension.metadata_address, Some(mint).try_into().unwrap()); + + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::InitializeMetadata.into(), + &mint.to_string(), + name, + symbol, + uri, + ], + ) + .await + .unwrap(); + + let account = config.rpc_client.get_account(&mint).await.unwrap(); + let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + let fetched_metadata = mint_state + .get_variable_len_extension::() + .unwrap(); + assert_eq!(fetched_metadata.name, name); + assert_eq!(fetched_metadata.symbol, symbol); + assert_eq!(fetched_metadata.uri, uri); + assert_eq!(fetched_metadata.mint, mint); + assert_eq!( + fetched_metadata.update_authority, + Some(payer.pubkey()).try_into().unwrap() + ); + assert_eq!(fetched_metadata.additional_metadata, []); + + // update canonical field + let new_value = "THIS!"; + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::UpdateMetadata.into(), + &mint.to_string(), + "NAME", + new_value, + ], + ) + .await + .unwrap(); + let account = config.rpc_client.get_account(&mint).await.unwrap(); + let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + let fetched_metadata = mint_state + .get_variable_len_extension::() + .unwrap(); + assert_eq!(fetched_metadata.name, new_value); + + // add new field + let field = "My field!"; + let value = "Try and stop me"; + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::UpdateMetadata.into(), + &mint.to_string(), + field, + value, + ], + ) + .await + .unwrap(); + let account = config.rpc_client.get_account(&mint).await.unwrap(); + let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + let fetched_metadata = mint_state + .get_variable_len_extension::() + .unwrap(); + assert_eq!( + fetched_metadata.additional_metadata, + [(field.to_string(), value.to_string())] + ); + + // remove it + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::UpdateMetadata.into(), + &mint.to_string(), + field, + "--remove", + ], + ) + .await + .unwrap(); + let account = config.rpc_client.get_account(&mint).await.unwrap(); + let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + let fetched_metadata = mint_state + .get_variable_len_extension::() + .unwrap(); + assert_eq!(fetched_metadata.additional_metadata, []); + + // fail to remove name + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::UpdateMetadata.into(), + &mint.to_string(), + "name", + "--remove", + ], + ) + .await + .unwrap_err(); + + // update authority + process_test_command( + &config, + &payer, + &[ + "spl-token", + CommandName::Authorize.into(), + &mint.to_string(), + "metadata", + &mint.to_string(), + ], + ) + .await + .unwrap(); + + let account = config.rpc_client.get_account(&mint).await.unwrap(); + let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + let fetched_metadata = mint_state + .get_variable_len_extension::() + .unwrap(); + assert_eq!( + fetched_metadata.update_authority, + Some(mint).try_into().unwrap() + ); +}