#![allow(deprecated)] // TODO: Remove when SPL upgrades to Solana 1.8 use clap::{ crate_description, crate_name, crate_version, value_t, value_t_or_exit, App, AppSettings, Arg, ArgMatches, SubCommand, }; use solana_account_decoder::{ parse_token::{TokenAccountType, UiAccountState}, UiAccountData, }; use 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_url_or_moniker, is_valid_pubkey, is_valid_signer, normalize_to_url_if_moniker, }, keypair::{signer_from_path, CliSignerInfo}, memo::memo_arg, nonce::*, offline::{self, *}, ArgConstant, }; use solana_cli_output::{ return_signers_with_config, CliSignature, OutputFormat, ReturnSignersConfig, }; use solana_client::{ blockhash_query::BlockhashQuery, rpc_client::RpcClient, rpc_request::TokenAccountsFilter, }; use solana_remote_wallet::remote_wallet::RemoteWalletManager; use solana_sdk::{ commitment_config::CommitmentConfig, instruction::Instruction, message::Message, native_token::*, program_option::COption, program_pack::Pack, pubkey::Pubkey, signature::{Keypair, Signer}, system_instruction, system_program, transaction::Transaction, }; use spl_associated_token_account::*; use spl_token::{ self, instruction::*, native_mint, state::{Account, Mint, Multisig}, }; use std::{collections::HashMap, process::exit, str::FromStr, sync::Arc}; mod config; use config::Config; mod output; use output::*; mod sort; use sort::sort_and_parse_token_accounts; mod rpc_client_utils; mod bench; use bench::*; pub const OWNER_ADDRESS_ARG: ArgConstant<'static> = ArgConstant { name: "owner", long: "owner", help: "Address of the token's owner. Defaults to the client keypair address.", }; pub const OWNER_KEYPAIR_ARG: ArgConstant<'static> = ArgConstant { name: "owner", long: "owner", help: "Keypair of the token's owner. 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 MULTISIG_SIGNER_ARG: ArgConstant<'static> = ArgConstant { name: "multisig_signer", long: "multisig-signer", help: "Member signer of a multisig account", }; 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) .requires(SIGN_ONLY_ARG.name) .requires(BLOCKHASH_ARG.name) .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) .requires(SIGN_ONLY_ARG.name) .requires(BLOCKHASH_ARG.name) .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) .requires(SIGN_ONLY_ARG.name) .requires(BLOCKHASH_ARG.name) .help(DELEGATE_ADDRESS_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(()) } } pub(crate) type Error = Box; type CommandResult = Result>)>, Error>; fn new_throwaway_signer() -> (Box, Pubkey) { let keypair = Keypair::new(); let pubkey = keypair.pubkey(); (Box::new(keypair) as Box, pubkey) } fn get_signer( matches: &ArgMatches<'_>, keypair_name: &str, wallet_manager: &mut Option>, ) -> Option<(Box, Pubkey)> { matches.value_of(keypair_name).map(|path| { let signer = signer_from_path(matches, path, keypair_name, wallet_manager).unwrap_or_else(|e| { eprintln!("error: {}", e); exit(1); }); let signer_pubkey = signer.pubkey(); (signer, signer_pubkey) }) } pub(crate) fn check_fee_payer_balance(config: &Config, required_balance: u64) -> Result<(), Error> { let balance = config.rpc_client.get_balance(&config.fee_payer)?; if balance < required_balance { Err(format!( "Fee payer, {}, has insufficient balance: {} required, {} available", config.fee_payer, lamports_to_sol(required_balance), lamports_to_sol(balance) ) .into()) } else { Ok(()) } } fn check_wallet_balance( config: &Config, wallet: &Pubkey, required_balance: u64, ) -> Result<(), Error> { let balance = config.rpc_client.get_balance(wallet)?; 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<(Box, 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((signer, signer_pubkey)); } Ok(Some(results)) } else { Ok(None) } } fn command_create_token( config: &Config, decimals: u8, token: Pubkey, authority: Pubkey, enable_freeze: bool, memo: Option, ) -> CommandResult { println_display(config, format!("Creating token {}", token)); let minimum_balance_for_rent_exemption = if !config.sign_only { config .rpc_client .get_minimum_balance_for_rent_exemption(Mint::LEN)? } else { 0 }; let freeze_authority_pubkey = if enable_freeze { Some(authority) } else { None }; let mut instructions = vec![ system_instruction::create_account( &config.fee_payer, &token, minimum_balance_for_rent_exemption, Mint::LEN as u64, &spl_token::id(), ), initialize_mint( &spl_token::id(), &token, &authority, freeze_authority_pubkey.as_ref(), decimals, )?, ]; if let Some(text) = memo { instructions.push(spl_memo::build_memo(text.as_bytes(), &[&config.fee_payer])); } Ok(Some(( minimum_balance_for_rent_exemption, vec![instructions], ))) } fn command_create_account( config: &Config, token: Pubkey, owner: Pubkey, maybe_account: Option, ) -> CommandResult { let minimum_balance_for_rent_exemption = if !config.sign_only { config .rpc_client .get_minimum_balance_for_rent_exemption(Account::LEN)? } else { 0 }; let (account, system_account_ok, instructions) = if let Some(account) = maybe_account { println_display(config, format!("Creating account {}", account)); ( account, false, vec![ system_instruction::create_account( &config.fee_payer, &account, minimum_balance_for_rent_exemption, Account::LEN as u64, &spl_token::id(), ), initialize_account(&spl_token::id(), &account, &token, &owner)?, ], ) } else { let account = get_associated_token_address(&owner, &token); println_display(config, format!("Creating account {}", account)); ( account, true, vec![create_associated_token_account( &config.fee_payer, &owner, &token, )], ) }; if !config.sign_only { if let Some(account_data) = config .rpc_client .get_account_with_commitment(&account, config.rpc_client.commitment())? .value { if !(account_data.owner == system_program::id() && system_account_ok) { return Err(format!("Error: Account already exists: {}", account).into()); } } } Ok(Some(( minimum_balance_for_rent_exemption, vec![instructions], ))) } fn command_create_multisig( config: &Config, multisig: Pubkey, minimum_signers: u8, multisig_members: Vec, ) -> CommandResult { println_display( config, format!( "Creating {}/{} multisig {}", minimum_signers, multisig_members.len(), multisig ), ); let minimum_balance_for_rent_exemption = if !config.sign_only { config .rpc_client .get_minimum_balance_for_rent_exemption(Multisig::LEN)? } else { 0 }; let instructions = vec![ system_instruction::create_account( &config.fee_payer, &multisig, minimum_balance_for_rent_exemption, Multisig::LEN as u64, &spl_token::id(), ), initialize_multisig( &spl_token::id(), &multisig, multisig_members.iter().collect::>().as_slice(), minimum_signers, )?, ]; Ok(Some(( minimum_balance_for_rent_exemption, vec![instructions], ))) } fn command_authorize( config: &Config, account: Pubkey, authority_type: AuthorityType, authority: Pubkey, new_authority: Option, force_authorize: bool, ) -> CommandResult { let auth_str = match authority_type { AuthorityType::MintTokens => "mint authority", AuthorityType::FreezeAccount => "freeze authority", AuthorityType::AccountOwner => "owner", AuthorityType::CloseAccount => "close authority", }; let previous_authority = if !config.sign_only { let target_account = config.rpc_client.get_account(&account)?; if let Ok(mint) = Mint::unpack(&target_account.data) { match authority_type { AuthorityType::AccountOwner | AuthorityType::CloseAccount => Err(format!( "Authority type `{}` not supported for SPL Token mints", auth_str )), AuthorityType::MintTokens => Ok(mint.mint_authority), AuthorityType::FreezeAccount => Ok(mint.freeze_authority), } } else if let Ok(token_account) = Account::unpack(&target_account.data) { let check_associated_token_account = || -> Result<(), Error> { let maybe_associated_token_account = get_associated_token_address(&token_account.owner, &token_account.mint); 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(()) } }; match authority_type { AuthorityType::MintTokens | AuthorityType::FreezeAccount => Err(format!( "Authority type `{}` not supported for SPL Token accounts", auth_str )), AuthorityType::AccountOwner => { check_associated_token_account()?; Ok(COption::Some(token_account.owner)) } AuthorityType::CloseAccount => { check_associated_token_account()?; Ok(COption::Some( token_account.close_authority.unwrap_or(token_account.owner), )) } } } else { Err("Unsupported account data format".to_string()) }? } else { COption::None }; println_display( config, format!( "Updating {}\n Current {}: {}\n New {}: {}", account, auth_str, previous_authority .map(|pubkey| pubkey.to_string()) .unwrap_or_else(|| "disabled".to_string()), auth_str, new_authority .map(|pubkey| pubkey.to_string()) .unwrap_or_else(|| "disabled".to_string()) ), ); let instructions = vec![set_authority( &spl_token::id(), &account, new_authority.as_ref(), authority_type, &authority, &config.multisigner_pubkeys, )?]; Ok(Some((0, vec![instructions]))) } pub(crate) fn resolve_mint_info( config: &Config, token_account: &Pubkey, mint_address: Option, mint_decimals: Option, ) -> Result<(Pubkey, u8), Error> { if !config.sign_only { let source_account = config .rpc_client .get_token_account(token_account)? .ok_or_else(|| format!("Could not find token account {}", token_account))?; let source_mint = Pubkey::from_str(&source_account.mint)?; if let Some(mint) = mint_address { if source_mint != mint { return Err(format!( "Source {:?} does not contain {:?} tokens", token_account, mint ) .into()); } } Ok((source_mint, source_account.token_amount.decimals)) } else { Ok(( mint_address.unwrap_or_default(), mint_decimals.unwrap_or_default(), )) } } fn validate_mint(config: &Config, token: Pubkey) -> Result<(), Error> { let mint = config.rpc_client.get_account(&token); if mint.is_err() || Mint::unpack(&mint.unwrap().data).is_err() { return Err(format!("Invalid mint account {:?}", token).into()); } Ok(()) } #[allow(clippy::too_many_arguments)] fn command_transfer( config: &Config, token: Pubkey, ui_amount: Option, recipient: Pubkey, sender: Option, sender_owner: Pubkey, allow_unfunded_recipient: bool, fund_recipient: bool, mint_decimals: Option, recipient_is_ata_owner: bool, use_unchecked_instruction: bool, memo: Option, ) -> CommandResult { let sender = if let Some(sender) = sender { sender } else { get_associated_token_address(&sender_owner, &token) }; let (mint_pubkey, decimals) = resolve_mint_info(config, &sender, Some(token), mint_decimals)?; let maybe_transfer_balance = ui_amount.map(|ui_amount| spl_token::ui_amount_to_amount(ui_amount, decimals)); let transfer_balance = if !config.sign_only { let sender_token_amount = config .rpc_client .get_token_account_balance(&sender) .map_err(|err| { format!( "Error: Failed to get token balance of sender address {}: {}", sender, err ) })?; let sender_balance = sender_token_amount.amount.parse::().map_err(|err| { format!( "Token account {} balance could not be parsed: {}", sender, err ) })?; let transfer_balance = maybe_transfer_balance.unwrap_or(sender_balance); println_display( config, format!( "Transfer {} tokens\n Sender: {}\n Recipient: {}", spl_token::amount_to_ui_amount(transfer_balance, decimals), sender, recipient ), ); if transfer_balance > sender_balance { return Err(format!( "Error: Sender has insufficient funds, current balance is {}", sender_token_amount.real_number_string_trimmed() ) .into()); } transfer_balance } else { maybe_transfer_balance.unwrap() }; let mut instructions = vec![]; let mut recipient_token_account = recipient; let mut minimum_balance_for_rent_exemption = 0; let recipient_is_token_account = if !config.sign_only { let recipient_account_info = config .rpc_client .get_account_with_commitment(&recipient, config.rpc_client.commitment())? .value .map(|account| account.owner == spl_token::id() && account.data.len() == Account::LEN); if recipient_account_info.is_none() && !allow_unfunded_recipient { return Err("Error: The recipient address is not funded. \ Add `--allow-unfunded-recipient` to complete the transfer \ " .into()); } recipient_account_info.unwrap_or(false) } else { !recipient_is_ata_owner }; if !recipient_is_token_account { recipient_token_account = get_associated_token_address(&recipient, &mint_pubkey); println_display( config, format!( " Recipient associated token account: {}", recipient_token_account ), ); let needs_funding = if !config.sign_only { if let Some(recipient_token_account_data) = config .rpc_client .get_account_with_commitment( &recipient_token_account, config.rpc_client.commitment(), )? .value { if recipient_token_account_data.owner == system_program::id() { true } else if recipient_token_account_data.owner == spl_token::id() { false } else { return Err( format!("Error: Unsupported recipient address: {}", recipient).into(), ); } } else { true } } else { fund_recipient }; if needs_funding { if fund_recipient { if !config.sign_only { minimum_balance_for_rent_exemption += config .rpc_client .get_minimum_balance_for_rent_exemption(Account::LEN)?; println_display( config, format!( " Funding recipient: {} ({} SOL)", recipient_token_account, lamports_to_sol(minimum_balance_for_rent_exemption) ), ); } instructions.push(create_associated_token_account( &config.fee_payer, &recipient, &mint_pubkey, )); } else { return Err( "Error: Recipient's associated token account does not exist. \ Add `--fund-recipient` to fund their account" .into(), ); } } } if use_unchecked_instruction { instructions.push(transfer( &spl_token::id(), &sender, &recipient_token_account, &sender_owner, &config.multisigner_pubkeys, transfer_balance, )?); } else { instructions.push(transfer_checked( &spl_token::id(), &sender, &mint_pubkey, &recipient_token_account, &sender_owner, &config.multisigner_pubkeys, transfer_balance, decimals, )?); } if let Some(text) = memo { instructions.push(spl_memo::build_memo(text.as_bytes(), &[&config.fee_payer])); } Ok(Some(( minimum_balance_for_rent_exemption, vec![instructions], ))) } #[allow(clippy::too_many_arguments)] fn command_burn( config: &Config, source: Pubkey, source_owner: Pubkey, ui_amount: f64, mint_address: Option, mint_decimals: Option, use_unchecked_instruction: bool, memo: Option, ) -> CommandResult { println_display( config, format!("Burn {} tokens\n Source: {}", ui_amount, source), ); let (mint_pubkey, decimals) = resolve_mint_info(config, &source, mint_address, mint_decimals)?; let amount = spl_token::ui_amount_to_amount(ui_amount, decimals); let mut instructions = if use_unchecked_instruction { vec![burn( &spl_token::id(), &source, &mint_pubkey, &source_owner, &config.multisigner_pubkeys, amount, )?] } else { vec![burn_checked( &spl_token::id(), &source, &mint_pubkey, &source_owner, &config.multisigner_pubkeys, amount, decimals, )?] }; if let Some(text) = memo { instructions.push(spl_memo::build_memo(text.as_bytes(), &[&config.fee_payer])); } Ok(Some((0, vec![instructions]))) } fn command_mint( config: &Config, token: Pubkey, ui_amount: f64, recipient: Pubkey, mint_decimals: Option, mint_authority: Pubkey, use_unchecked_instruction: bool, ) -> CommandResult { println_display( config, format!( "Minting {} tokens\n Token: {}\n Recipient: {}", ui_amount, token, recipient ), ); let (_, decimals) = resolve_mint_info(config, &recipient, None, mint_decimals)?; let amount = spl_token::ui_amount_to_amount(ui_amount, decimals); let instructions = if use_unchecked_instruction { vec![mint_to( &spl_token::id(), &token, &recipient, &mint_authority, &config.multisigner_pubkeys, amount, )?] } else { vec![mint_to_checked( &spl_token::id(), &token, &recipient, &mint_authority, &config.multisigner_pubkeys, amount, decimals, )?] }; Ok(Some((0, vec![instructions]))) } fn command_freeze( config: &Config, account: Pubkey, mint_address: Option, freeze_authority: Pubkey, ) -> CommandResult { let (token, _) = resolve_mint_info(config, &account, mint_address, None)?; println_display( config, format!("Freezing account: {}\n Token: {}", account, token), ); let instructions = vec![freeze_account( &spl_token::id(), &account, &token, &freeze_authority, &config.multisigner_pubkeys, )?]; Ok(Some((0, vec![instructions]))) } fn command_thaw( config: &Config, account: Pubkey, mint_address: Option, freeze_authority: Pubkey, ) -> CommandResult { let (token, _) = resolve_mint_info(config, &account, mint_address, None)?; println_display( config, format!("Freezing account: {}\n Token: {}", account, token), ); let instructions = vec![thaw_account( &spl_token::id(), &account, &token, &freeze_authority, &config.multisigner_pubkeys, )?]; Ok(Some((0, vec![instructions]))) } fn command_wrap( config: &Config, sol: f64, wallet_address: Pubkey, wrapped_sol_account: Option, ) -> CommandResult { let lamports = sol_to_lamports(sol); let instructions = if let Some(wrapped_sol_account) = wrapped_sol_account { println_display( config, format!("Wrapping {} SOL into {}", sol, wrapped_sol_account), ); vec![ system_instruction::create_account( &wallet_address, &wrapped_sol_account, lamports, Account::LEN as u64, &spl_token::id(), ), initialize_account( &spl_token::id(), &wrapped_sol_account, &native_mint::id(), &wallet_address, )?, ] } else { let account = get_associated_token_address(&wallet_address, &native_mint::id()); if !config.sign_only { if let Some(account_data) = config .rpc_client .get_account_with_commitment(&account, config.rpc_client.commitment())? .value { if account_data.owner != system_program::id() { return Err(format!("Error: Account already exists: {}", account).into()); } } } println_display(config, format!("Wrapping {} SOL into {}", sol, account)); vec![ system_instruction::transfer(&wallet_address, &account, lamports), create_associated_token_account(&config.fee_payer, &wallet_address, &native_mint::id()), ] }; if !config.sign_only { check_wallet_balance(config, &wallet_address, lamports)?; } Ok(Some((0, vec![instructions]))) } fn command_unwrap( config: &Config, wallet_address: Pubkey, address: Option, ) -> CommandResult { let use_associated_account = address.is_none(); let address = address .unwrap_or_else(|| get_associated_token_address(&wallet_address, &native_mint::id())); println_display(config, format!("Unwrapping {}", address)); if !config.sign_only { let lamports = config.rpc_client.get_balance(&address)?; if 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 {}", address).into()); } } println_display( config, format!(" Amount: {} SOL", lamports_to_sol(lamports)), ); } println_display(config, format!(" Recipient: {}", &wallet_address)); let instructions = vec![close_account( &spl_token::id(), &address, &wallet_address, &wallet_address, &config.multisigner_pubkeys, )?]; Ok(Some((0, vec![instructions]))) } #[allow(clippy::too_many_arguments)] fn command_approve( config: &Config, account: Pubkey, owner: Pubkey, ui_amount: f64, delegate: Pubkey, mint_address: Option, mint_decimals: Option, use_unchecked_instruction: bool, ) -> CommandResult { println_display( config, format!( "Approve {} tokens\n Account: {}\n Delegate: {}", ui_amount, account, delegate ), ); let (mint_pubkey, decimals) = resolve_mint_info(config, &account, mint_address, mint_decimals)?; let amount = spl_token::ui_amount_to_amount(ui_amount, decimals); let instructions = if use_unchecked_instruction { vec![approve( &spl_token::id(), &account, &delegate, &owner, &config.multisigner_pubkeys, amount, )?] } else { vec![approve_checked( &spl_token::id(), &account, &mint_pubkey, &delegate, &owner, &config.multisigner_pubkeys, amount, decimals, )?] }; Ok(Some((0, vec![instructions]))) } fn command_revoke( config: &Config, account: Pubkey, owner: Pubkey, delegate: Option, ) -> CommandResult { let delegate = if !config.sign_only { let source_account = config .rpc_client .get_token_account(&account)? .ok_or_else(|| format!("Could not find token account {}", account))?; if let Some(string) = source_account.delegate { Some(Pubkey::from_str(&string)?) } else { None } } else { 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 instructions = vec![revoke( &spl_token::id(), &account, &owner, &config.multisigner_pubkeys, )?]; Ok(Some((0, vec![instructions]))) } fn command_close( config: &Config, account: Pubkey, close_authority: Pubkey, recipient: Pubkey, ) -> CommandResult { if !config.sign_only { let source_account = config .rpc_client .get_token_account(&account)? .ok_or_else(|| format!("Could not find token account {}", account))?; let source_amount = source_account .token_amount .amount .parse::() .map_err(|err| { format!( "Token account {} balance could not be parsed: {}", account, err ) })?; if !source_account.is_native && source_amount > 0 { return Err(format!( "Account {} still has {} tokens; empty the account in order to close it.", account, source_account.token_amount.real_number_string_trimmed() ) .into()); } } let instructions = vec![close_account( &spl_token::id(), &account, &recipient, &close_authority, &config.multisigner_pubkeys, )?]; Ok(Some((0, vec![instructions]))) } fn command_balance(config: &Config, address: Pubkey) -> CommandResult { let balance = config .rpc_client .get_token_account_balance(&address) .map_err(|_| format!("Could not find token account {}", address))?; let cli_token_amount = CliTokenAmount { amount: balance }; println!( "{}", config.output_format.formatted_string(&cli_token_amount) ); Ok(None) } fn command_supply(config: &Config, address: Pubkey) -> CommandResult { let supply = config.rpc_client.get_token_supply(&address)?; let cli_token_amount = CliTokenAmount { amount: supply }; println!( "{}", config.output_format.formatted_string(&cli_token_amount) ); Ok(None) } fn command_accounts(config: &Config, token: Option, owner: Pubkey) -> CommandResult { if let Some(token) = token { validate_mint(config, token)?; } let accounts = config.rpc_client.get_token_accounts_by_owner( &owner, match token { Some(token) => TokenAccountsFilter::Mint(token), None => TokenAccountsFilter::ProgramId(spl_token::id()), }, )?; if accounts.is_empty() { println!("None"); return Ok(None); } let (mint_accounts, unsupported_accounts, max_len_balance, includes_aux) = sort_and_parse_token_accounts(&owner, accounts); let aux_len = if includes_aux { 10 } else { 0 }; let cli_token_accounts = CliTokenAccounts { accounts: mint_accounts .into_iter() .map(|(_mint, accounts_list)| accounts_list) .collect(), unsupported_accounts, max_len_balance, aux_len, token_is_some: token.is_some(), }; println!( "{}", config.output_format.formatted_string(&cli_token_accounts) ); Ok(None) } 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 { validate_mint(config, token)?; let associated_token_address = get_associated_token_address(&owner, &token); cli_address.associated_token_address = Some(associated_token_address.to_string()); } println!("{}", config.output_format.formatted_string(&cli_address)); Ok(None) } fn command_account_info(config: &Config, address: Pubkey) -> CommandResult { let account = config .rpc_client .get_token_account(&address) .map_err(|_| format!("Could not find token account {}", address))? .unwrap(); let mint = Pubkey::from_str(&account.mint).unwrap(); let owner = Pubkey::from_str(&account.owner).unwrap(); let is_associated = get_associated_token_address(&owner, &mint) == address; let cli_token_account = CliTokenAccount { address: address.to_string(), is_associated, account, }; println!( "{}", config.output_format.formatted_string(&cli_token_account) ); Ok(None) } fn get_multisig(config: &Config, address: &Pubkey) -> Result { let account = config.rpc_client.get_account(address)?; Multisig::unpack(&account.data).map_err(|e| e.into()) } fn command_multisig(config: &Config, address: Pubkey) -> CommandResult { let multisig = get_multisig(config, &address)?; let n = multisig.n as usize; assert!(n <= multisig.signers.len()); let cli_multisig = CliMultisig { address: address.to_string(), m: multisig.m, n: multisig.n, signers: multisig .signers .iter() .enumerate() .filter_map(|(i, signer)| { if i < n { Some(signer.to_string()) } else { None } }) .collect(), }; println!("{}", config.output_format.formatted_string(&cli_multisig)); Ok(None) } fn command_gc(config: &Config, owner: Pubkey) -> CommandResult { println_display(config, "Fetching token accounts".to_string()); let accounts = config .rpc_client .get_token_accounts_by_owner(&owner, TokenAccountsFilter::ProgramId(spl_token::id()))?; if accounts.is_empty() { println_display(config, "Nothing to do".to_string()); return Ok(None); } let minimum_balance_for_rent_exemption = if !config.sign_only { config .rpc_client .get_minimum_balance_for_rent_exemption(Account::LEN)? } else { 0 }; let mut accounts_by_token = HashMap::new(); for keyed_account in accounts { if let UiAccountData::Json(parsed_account) = keyed_account.account.data { if parsed_account.program == "spl-token" { if let Ok(TokenAccountType::Account(ui_token_account)) = serde_json::from_value(parsed_account.parsed) { let frozen = ui_token_account.state == UiAccountState::Frozen; 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).or_insert_with(HashMap::new); entry.insert( token_account, ( token_amount, ui_token_account.token_amount.decimals, frozen, close_authority, ), ); } } } } let mut instructions = vec![]; let mut lamports_needed = 0; for (token, accounts) in accounts_by_token.into_iter() { println_display(config, format!("Processing token: {}", token)); let associated_token_account = get_associated_token_address(&owner, &token); let total_balance: u64 = accounts.values().map(|account| account.0).sum(); if total_balance > 0 && !accounts.contains_key(&associated_token_account) { // Create the associated token account instructions.push(vec![create_associated_token_account( &config.fee_payer, &owner, &token, )]); lamports_needed += minimum_balance_for_rent_exemption; } for (address, (amount, decimals, frozen, close_authority)) in accounts { if address == associated_token_account { // leave the associated token account alone continue; } if frozen { // leave frozen accounts alone continue; } let mut account_instructions = vec![]; // Transfer the account balance into the associated token account if amount > 0 { account_instructions.push(transfer_checked( &spl_token::id(), &address, &token, &associated_token_account, &owner, &config.multisigner_pubkeys, amount, decimals, )?); } // Close the account if config.owner is able to if close_authority == owner { account_instructions.push(close_account( &spl_token::id(), &address, &owner, &owner, &config.multisigner_pubkeys, )?); } if !account_instructions.is_empty() { instructions.push(account_instructions); } } } Ok(Some((lamports_needed, instructions))) } fn command_sync_native(native_account_address: Pubkey) -> CommandResult { let instructions = vec![sync_native(&spl_token::id(), &native_account_address)?]; Ok(Some((0, vec![instructions]))) } 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]) } } 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]) } } 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]) } } 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 main() { let default_decimals = &format!("{}", native_mint::DECIMALS); let mut no_wait = false; let app_matches = App::new(crate_name!()) .about(crate_description!()) .version(crate_version!()) .setting(AppSettings::SubcommandRequiredElseHelp) .arg({ let arg = Arg::with_name("config_file") .short("C") .long("config") .value_name("PATH") .takes_value(true) .global(true) .help("Configuration file to use"); if let Some(ref config_file) = *solana_cli_config::CONFIG_FILE { arg.default_value(config_file) } else { arg } }) .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("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("create-token").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 associated token accounts." ), ) .nonce_args(true) .arg(memo_arg()) .offline_args(), ) .subcommand( SubCommand::with_name("create-account") .about("Create a new token account") .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 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(owner_address_arg()) .nonce_args(true) .offline_args(), ) .subcommand( SubCommand::with_name("create-multisig") .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(&format!("The minimum number of signers required \ to allow the operation. [{} <= M <= N]", MIN_SIGNERS, )), ) .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(&format!("The public keys for each of the N \ signing members of this account. [{} <= N <= {}]", MIN_SIGNERS, MAX_SIGNERS, )), ) .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) .offline_args(), ) .subcommand( SubCommand::with_name("authorize") .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 account"), ) .arg( Arg::with_name("authority_type") .value_name("AUTHORITY_TYPE") .takes_value(true) .possible_values(&["mint", "freeze", "owner", "close"]) .index(2) .required(true) .help("The new authority type. \ Token mints support `mint` and `freeze` authorities;\ Token accounts support `owner` and `close` 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("transfer") .about("Transfer tokens between accounts") .arg( Arg::with_name("token") .validator(is_valid_pubkey) .value_name("TOKEN_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_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, 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) .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("recipient_is_ata_owner") .long("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(multisig_signer_arg()) .arg(mint_decimals_arg()) .nonce_args(true) .arg(memo_arg()) .offline_args_config(&SignOnlyNeedsMintDecimals{}), ) .subcommand( SubCommand::with_name("burn") .about("Burn tokens from an account") .arg( Arg::with_name("source") .validator(is_valid_pubkey) .value_name("SOURCE_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("SOURCE_TOKEN_OWNER_KEYPAIR") .help( "Specify the source token owner account. \ This may be a keypair file, 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("mint") .about("Mint new tokens") .arg( Arg::with_name("token") .validator(is_valid_pubkey) .value_name("TOKEN_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) .index(3) .help("The token account address of recipient [default: associated token account for --owner]"), ) .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) .offline_args_config(&SignOnlyNeedsMintDecimals{}), ) .subcommand( SubCommand::with_name("freeze") .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("thaw") .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("wrap") .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 auxillary account instead of associated token account"), ) .nonce_args(true) .offline_args(), ) .subcommand( SubCommand::with_name("unwrap") .about("Unwrap a SOL token account") .arg( Arg::with_name("address") .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(multisig_signer_arg()) .nonce_args(true) .offline_args(), ) .subcommand( SubCommand::with_name("approve") .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("revoke") .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("close") .about("Close a token account") .arg( Arg::with_name("token") .validator(is_valid_pubkey) .value_name("TOKEN_ADDRESS") .takes_value(true) .index(1) .required_unless("address") .help("Token to close. To close a specific account, use the `--address` parameter instead"), ) .arg(owner_address_arg()) .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, 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(multisig_signer_arg()) .nonce_args(true) .offline_args(), ) .subcommand( SubCommand::with_name("balance") .about("Get token account balance") .arg( Arg::with_name("token") .validator(is_valid_pubkey) .value_name("TOKEN_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("supply") .about("Get token supply") .arg( Arg::with_name("address") .validator(is_valid_pubkey) .value_name("TOKEN_ADDRESS") .takes_value(true) .index(1) .required(true) .help("The token address"), ), ) .subcommand( SubCommand::with_name("accounts") .about("List all token accounts by owner") .arg( Arg::with_name("token") .validator(is_valid_pubkey) .value_name("TOKEN_ADDRESS") .takes_value(true) .index(1) .help("Limit results to the given token. [Default: list accounts for all tokens]"), ) .arg(owner_address_arg()) ) .subcommand( SubCommand::with_name("address") .about("Get wallet address") .arg( Arg::with_name("token") .validator(is_valid_pubkey) .value_name("TOKEN_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("account-info") .about("Query details of an SPL Token account by address") .arg( Arg::with_name("token") .validator(is_valid_pubkey) .value_name("TOKEN_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( owner_address_arg() .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("multisig-info") .about("Query details about and SPL Token multisig account by address") .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("gc") .about("Cleanup unnecessary token accounts") .arg(owner_keypair_arg()) ) .subcommand( SubCommand::with_name("sync-native") .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"), ), ) .get_matches(); let mut wallet_manager = None; let mut bulk_signers: Vec> = Vec::new(); let mut multisigner_ids = Vec::new(); let (sub_command, sub_matches) = app_matches.subcommand(); let matches = sub_matches.unwrap(); let config = { let cli_config = if let Some(config_file) = matches.value_of("config_file") { solana_cli_config::Config::load(config_file).unwrap_or_default() } else { solana_cli_config::Config::default() }; let json_rpc_url = normalize_to_url_if_moniker( matches .value_of("json_rpc_url") .unwrap_or(&cli_config.json_rpc_url), ); let websocket_url = solana_cli_config::Config::compute_websocket_url(&json_rpc_url); let (signer, fee_payer) = signer_from_path( matches, matches .value_of("fee_payer") .unwrap_or(&cli_config.keypair_path), "fee_payer", &mut wallet_manager, ) .map(|s| { let p = s.pubkey(); (s, p) }) .unwrap_or_else(|e| { eprintln!("error: {}", e); exit(1); }); bulk_signers.push(signer); let verbose = matches.is_present("verbose"); let output_format = matches .value_of("output_format") .map(|value| match value { "json" => OutputFormat::Json, "json-compact" => OutputFormat::JsonCompact, _ => unreachable!(), }) .unwrap_or(if verbose { OutputFormat::DisplayVerbose } else { OutputFormat::Display }); let nonce_account = pubkey_of_signer(matches, NONCE_ARG.name, &mut wallet_manager) .unwrap_or_else(|e| { eprintln!("error: {}", e); exit(1); }); let nonce_authority = if nonce_account.is_some() { let (signer, nonce_authority) = signer_from_path( matches, matches .value_of(NONCE_AUTHORITY_ARG.name) .unwrap_or(&cli_config.keypair_path), NONCE_AUTHORITY_ARG.name, &mut wallet_manager, ) .map(|s| { let p = s.pubkey(); (s, p) }) .unwrap_or_else(|e| { eprintln!("error: {}", e); exit(1); }); bulk_signers.push(signer); Some(nonce_authority) } else { None }; let blockhash_query = BlockhashQuery::new_from_matches(matches); let sign_only = matches.is_present(SIGN_ONLY_ARG.name); let dump_transaction_message = matches.is_present(DUMP_TRANSACTION_MESSAGE.name); let multisig_signers = signers_of(matches, MULTISIG_SIGNER_ARG.name, &mut wallet_manager) .unwrap_or_else(|e| { eprintln!("error: {}", e); exit(1); }); if let Some(mut multisig_signers) = multisig_signers { multisig_signers.sort_by(|(_, lp), (_, rp)| lp.cmp(rp)); let (signers, pubkeys): (Vec<_>, Vec<_>) = multisig_signers.into_iter().unzip(); bulk_signers.extend(signers); multisigner_ids = pubkeys; } let multisigner_pubkeys = multisigner_ids.iter().collect::>(); Config { rpc_client: Arc::new(RpcClient::new_with_commitment( json_rpc_url, CommitmentConfig::confirmed(), )), websocket_url, output_format, fee_payer, default_keypair_path: cli_config.keypair_path, nonce_account, nonce_authority, blockhash_query, sign_only, dump_transaction_message, multisigner_pubkeys, } }; solana_logger::setup_with_default("solana=info"); let _ = match (sub_command, sub_matches) { ("bench", Some(arg_matches)) => bench_process_command( arg_matches, &config, std::mem::take(&mut bulk_signers), &mut wallet_manager, ) .map(|_| None), ("create-token", Some(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 (token_signer, token) = get_signer(arg_matches, "token_keypair", &mut wallet_manager) .unwrap_or_else(new_throwaway_signer); bulk_signers.push(token_signer); command_create_token( &config, decimals, token, mint_authority, arg_matches.is_present("enable_freeze"), memo, ) } ("create-account", Some(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)| { bulk_signers.push(signer); account }, ); let owner = config.pubkey_or_default(arg_matches, "owner", &mut wallet_manager); command_create_account(&config, token, owner, account) } ("create-multisig", Some(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(|e| { eprintln!("error: {}", e); exit(1); }) .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, account) = get_signer(arg_matches, "address_keypair", &mut wallet_manager) .unwrap_or_else(new_throwaway_signer); bulk_signers.push(signer); command_create_multisig(&config, account, minimum_signers, multisig_members) } ("authorize", Some(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 = match authority_type { "mint" => AuthorityType::MintTokens, "freeze" => AuthorityType::FreezeAccount, "owner" => AuthorityType::AccountOwner, "close" => AuthorityType::CloseAccount, _ => unreachable!(), }; let (authority_signer, authority) = config.signer_or_default(arg_matches, "authority", &mut wallet_manager); bulk_signers.push(authority_signer); 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, ) } ("transfer", Some(arg_matches)) => { let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) .unwrap() .unwrap(); let amount = match 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); bulk_signers.push(owner_signer); let mint_decimals = value_of::(arg_matches, MINT_DECIMALS_ARG.name); let fund_recipient = matches.is_present("fund_recipient"); let allow_unfunded_recipient = matches.is_present("allow_empty_recipient") || matches.is_present("allow_unfunded_recipient"); no_wait = matches.is_present("no_wait"); let recipient_is_ata_owner = matches.is_present("recipient_is_ata_owner"); let use_unchecked_instruction = matches.is_present("use_unchecked_instruction"); let memo = value_t!(arg_matches, "memo", String).ok(); command_transfer( &config, token, amount, recipient, sender, owner, allow_unfunded_recipient, fund_recipient, mint_decimals, recipient_is_ata_owner, use_unchecked_instruction, memo, ) } ("burn", Some(arg_matches)) => { let source = pubkey_of_signer(arg_matches, "source", &mut wallet_manager) .unwrap() .unwrap(); let (owner_signer, owner) = config.signer_or_default(arg_matches, "owner", &mut wallet_manager); bulk_signers.push(owner_signer); 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 = matches.is_present("use_unchecked_instruction"); let memo = value_t!(arg_matches, "memo", String).ok(); command_burn( &config, source, owner, amount, mint_address, mint_decimals, use_unchecked_instruction, memo, ) } ("mint", Some(arg_matches)) => { let (mint_authority_signer, mint_authority) = config.signer_or_default(arg_matches, "mint_authority", &mut wallet_manager); bulk_signers.push(mint_authority_signer); let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) .unwrap() .unwrap(); let amount = value_t_or_exit!(arg_matches, "amount", f64); let recipient = config.associated_token_address_or_override( arg_matches, "recipient", &mut wallet_manager, ); let mint_decimals = value_of::(arg_matches, MINT_DECIMALS_ARG.name); let use_unchecked_instruction = matches.is_present("use_unchecked_instruction"); command_mint( &config, token, amount, recipient, mint_decimals, mint_authority, use_unchecked_instruction, ) } ("freeze", Some(arg_matches)) => { let (freeze_authority_signer, freeze_authority) = config.signer_or_default(arg_matches, "freeze_authority", &mut wallet_manager); bulk_signers.push(freeze_authority_signer); 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) } ("thaw", Some(arg_matches)) => { let (freeze_authority_signer, freeze_authority) = config.signer_or_default(arg_matches, "freeze_authority", &mut wallet_manager); bulk_signers.push(freeze_authority_signer); 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) } ("wrap", Some(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); bulk_signers.push(wallet_signer); command_wrap(&config, amount, wallet_address, account) } ("unwrap", Some(arg_matches)) => { let (wallet_signer, wallet_address) = config.signer_or_default(arg_matches, "wallet_keypair", &mut wallet_manager); bulk_signers.push(wallet_signer); let address = pubkey_of_signer(arg_matches, "address", &mut wallet_manager).unwrap(); command_unwrap(&config, wallet_address, address) } ("approve", Some(arg_matches)) => { let (owner_signer, owner_address) = config.signer_or_default(arg_matches, "owner", &mut wallet_manager); bulk_signers.push(owner_signer); 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 = matches.is_present("use_unchecked_instruction"); command_approve( &config, account, owner_address, amount, delegate, mint_address, mint_decimals, use_unchecked_instruction, ) } ("revoke", Some(arg_matches)) => { let (owner_signer, owner_address) = config.signer_or_default(arg_matches, "owner", &mut wallet_manager); bulk_signers.push(owner_signer); 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) } ("close", Some(arg_matches)) => { let (close_authority_signer, close_authority) = config.signer_or_default(arg_matches, "close_authority", &mut wallet_manager); bulk_signers.push(close_authority_signer); let address = config.associated_token_address_or_override( arg_matches, "address", &mut wallet_manager, ); let recipient = config.pubkey_or_default(arg_matches, "recipient", &mut wallet_manager); command_close(&config, address, close_authority, recipient) } ("balance", Some(arg_matches)) => { let address = config.associated_token_address_or_override( arg_matches, "address", &mut wallet_manager, ); command_balance(&config, address) } ("supply", Some(arg_matches)) => { let address = pubkey_of_signer(arg_matches, "address", &mut wallet_manager) .unwrap() .unwrap(); command_supply(&config, address) } ("accounts", Some(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_accounts(&config, token, owner) } ("address", Some(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) } ("account-info", Some(arg_matches)) => { let address = config.associated_token_address_or_override( arg_matches, "address", &mut wallet_manager, ); command_account_info(&config, address) } ("multisig-info", Some(arg_matches)) => { let address = pubkey_of_signer(arg_matches, "address", &mut wallet_manager) .unwrap() .unwrap(); command_multisig(&config, address) } ("gc", Some(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 (owner_signer, owner_address) = config.signer_or_default(arg_matches, "owner", &mut wallet_manager); bulk_signers.push(owner_signer); command_gc(&config, owner_address) } ("sync-native", Some(arg_matches)) => { let address = config.associated_token_address_for_token_or_override( arg_matches, "address", &mut wallet_manager, Some(native_mint::id()), ); command_sync_native(address) } _ => unreachable!(), } .and_then(|transaction_info| { if let Some((minimum_balance_for_rent_exemption, instruction_batches)) = transaction_info { let fee_payer = Some(&config.fee_payer); let signer_info = CliSignerInfo { signers: bulk_signers, }; for instructions in instruction_batches { let message = if let Some(nonce_account) = config.nonce_account.as_ref() { Message::new_with_nonce( instructions, fee_payer, nonce_account, config.nonce_authority.as_ref().unwrap(), ) } else { Message::new(&instructions, fee_payer) }; let (recent_blockhash, fee_calculator) = config .blockhash_query .get_blockhash_and_fee_calculator( &config.rpc_client, config.rpc_client.commitment(), ) .unwrap_or_else(|e| { eprintln!("error: {}", e); exit(1); }); if !config.sign_only { check_fee_payer_balance( &config, minimum_balance_for_rent_exemption + fee_calculator.calculate_fee(&message), )?; } let signers = signer_info.signers_for_message(&message); let mut transaction = Transaction::new_unsigned(message); if config.sign_only { transaction.try_partial_sign(&signers, recent_blockhash)?; println!( "{}", return_signers_with_config( &transaction, &config.output_format, &ReturnSignersConfig { dump_transaction_message: config.dump_transaction_message, } )? ); } else { transaction.try_sign(&signers, recent_blockhash)?; let signature = if no_wait { config.rpc_client.send_transaction(&transaction)? } else { config .rpc_client .send_and_confirm_transaction_with_spinner(&transaction)? }; let signature = CliSignature { signature: signature.to_string(), }; println!("{}", config.output_format.formatted_string(&signature)); } } } Ok(()) }) .map_err(|err| { eprintln!("{}", err); exit(1); }); }