use crate::{ checks::*, cli_output::{CliAccount, CliSignOnlyData, CliSignature, OutputFormat}, cluster_query::*, display::println_name_value, nonce::{self, *}, offline::{blockhash_query::BlockhashQuery, *}, spend_utils::*, stake::*, validator_info::*, vote::*, }; use chrono::prelude::*; use clap::{App, AppSettings, Arg, ArgMatches, SubCommand}; use log::*; use num_traits::FromPrimitive; use serde_json::{self, json, Value}; use solana_budget_program::budget_instruction::{self, BudgetError}; use solana_clap_utils::{ commitment::{commitment_arg_with_default, COMMITMENT_ARG}, input_parsers::*, input_validators::*, keypair::signer_from_path, offline::SIGN_ONLY_ARG, ArgConstant, }; use solana_client::{ client_error::{ClientError, ClientErrorKind, Result as ClientResult}, rpc_client::RpcClient, rpc_config::RpcLargestAccountsFilter, rpc_response::{RpcAccount, RpcKeyedAccount}, }; #[cfg(not(test))] use solana_faucet::faucet::request_airdrop_transaction; #[cfg(test)] use solana_faucet::faucet_mock::request_airdrop_transaction; use solana_remote_wallet::remote_wallet::RemoteWalletManager; use solana_sdk::{ bpf_loader, clock::{Epoch, Slot}, commitment_config::CommitmentConfig, fee_calculator::FeeCalculator, hash::Hash, instruction::InstructionError, loader_instruction, message::Message, native_token::lamports_to_sol, program_utils::DecodeError, pubkey::{Pubkey, MAX_SEED_LEN}, signature::{Keypair, Signature, Signer, SignerError}, system_instruction::{self, SystemError}, system_program, transaction::{Transaction, TransactionError}, }; use solana_stake_program::{ stake_instruction::LockupArgs, stake_state::{Lockup, StakeAuthorize}, }; use solana_transaction_status::{EncodedTransaction, TransactionEncoding}; use solana_vote_program::vote_state::VoteAuthorize; use std::{ error, fmt::Write as FmtWrite, fs::File, io::{Read, Write}, net::{IpAddr, SocketAddr}, sync::Arc, thread::sleep, time::Duration, }; use thiserror::Error; use url::Url; pub type CliSigners = Vec>; pub type SignerIndex = usize; pub(crate) struct CliSignerInfo { pub signers: CliSigners, } impl CliSignerInfo { pub(crate) fn index_of(&self, pubkey: Option) -> Option { if let Some(pubkey) = pubkey { self.signers .iter() .position(|signer| signer.pubkey() == pubkey) } else { Some(0) } } } pub(crate) fn generate_unique_signers( bulk_signers: Vec>>, matches: &ArgMatches<'_>, default_signer_path: &str, wallet_manager: &mut Option>, ) -> Result> { let mut unique_signers = vec![]; // Determine if the default signer is needed if bulk_signers.iter().any(|signer| signer.is_none()) { let default_signer = signer_from_path(matches, default_signer_path, "keypair", wallet_manager)?; unique_signers.push(default_signer); } for signer in bulk_signers.into_iter() { if let Some(signer) = signer { if !unique_signers.iter().any(|s| s == &signer) { unique_signers.push(signer); } } } Ok(CliSignerInfo { signers: unique_signers, }) } const DATA_CHUNK_SIZE: usize = 229; // Keep program chunks under PACKET_DATA_SIZE pub const FEE_PAYER_ARG: ArgConstant<'static> = ArgConstant { name: "fee_payer", long: "fee-payer", help: "Specify the fee-payer account. This may be a keypair file, the ASK keyword \n\ or the pubkey of an offline signer, provided an appropriate --signer argument \n\ is also passed. Defaults to the client keypair.", }; pub fn fee_payer_arg<'a, 'b>() -> Arg<'a, 'b> { Arg::with_name(FEE_PAYER_ARG.name) .long(FEE_PAYER_ARG.long) .takes_value(true) .value_name("KEYPAIR") .validator(is_valid_signer) .help(FEE_PAYER_ARG.help) } #[derive(Debug)] pub struct KeypairEq(Keypair); impl From for KeypairEq { fn from(keypair: Keypair) -> Self { Self(keypair) } } impl PartialEq for KeypairEq { fn eq(&self, other: &Self) -> bool { self.pubkey() == other.pubkey() } } impl std::ops::Deref for KeypairEq { type Target = Keypair; fn deref(&self) -> &Self::Target { &self.0 } } pub fn nonce_authority_arg<'a, 'b>() -> Arg<'a, 'b> { nonce::nonce_authority_arg().requires(NONCE_ARG.name) } #[derive(Default, Debug, PartialEq)] pub struct PayCommand { pub amount: SpendAmount, pub to: Pubkey, pub timestamp: Option>, pub timestamp_pubkey: Option, pub witnesses: Option>, pub cancelable: bool, pub sign_only: bool, pub blockhash_query: BlockhashQuery, pub nonce_account: Option, pub nonce_authority: SignerIndex, } #[derive(Debug, PartialEq)] #[allow(clippy::large_enum_variant)] pub enum CliCommand { // Cluster Query Commands Catchup { node_pubkey: Pubkey, node_json_rpc_url: Option, commitment_config: CommitmentConfig, follow: bool, }, ClusterDate, ClusterVersion, CreateAddressWithSeed { from_pubkey: Option, seed: String, program_id: Pubkey, }, Fees, GetBlockTime { slot: Option, }, GetEpochInfo { commitment_config: CommitmentConfig, }, GetGenesisHash, GetEpoch { commitment_config: CommitmentConfig, }, GetSlot { commitment_config: CommitmentConfig, }, LargestAccounts { commitment_config: CommitmentConfig, filter: Option, }, Supply { commitment_config: CommitmentConfig, print_accounts: bool, }, TotalSupply { commitment_config: CommitmentConfig, }, GetTransactionCount { commitment_config: CommitmentConfig, }, LeaderSchedule, LiveSlots, Ping { lamports: u64, interval: Duration, count: Option, timeout: Duration, commitment_config: CommitmentConfig, }, ShowBlockProduction { epoch: Option, slot_limit: Option, }, ShowGossip, ShowStakes { use_lamports_unit: bool, vote_account_pubkeys: Option>, }, ShowValidators { use_lamports_unit: bool, commitment_config: CommitmentConfig, }, TransactionHistory { address: Pubkey, end_slot: Option, // None == latest slot slot_limit: u64, }, // Nonce commands AuthorizeNonceAccount { nonce_account: Pubkey, nonce_authority: SignerIndex, new_authority: Pubkey, }, CreateNonceAccount { nonce_account: SignerIndex, seed: Option, nonce_authority: Option, amount: SpendAmount, }, GetNonce(Pubkey), NewNonce { nonce_account: Pubkey, nonce_authority: SignerIndex, }, ShowNonceAccount { nonce_account_pubkey: Pubkey, use_lamports_unit: bool, }, WithdrawFromNonceAccount { nonce_account: Pubkey, nonce_authority: SignerIndex, destination_account_pubkey: Pubkey, lamports: u64, }, // Program Deployment Deploy(String), // Stake Commands CreateStakeAccount { stake_account: SignerIndex, seed: Option, staker: Option, withdrawer: Option, lockup: Lockup, amount: SpendAmount, sign_only: bool, blockhash_query: BlockhashQuery, nonce_account: Option, nonce_authority: SignerIndex, fee_payer: SignerIndex, from: SignerIndex, }, DeactivateStake { stake_account_pubkey: Pubkey, stake_authority: SignerIndex, sign_only: bool, blockhash_query: BlockhashQuery, nonce_account: Option, nonce_authority: SignerIndex, fee_payer: SignerIndex, }, DelegateStake { stake_account_pubkey: Pubkey, vote_account_pubkey: Pubkey, stake_authority: SignerIndex, force: bool, sign_only: bool, blockhash_query: BlockhashQuery, nonce_account: Option, nonce_authority: SignerIndex, fee_payer: SignerIndex, }, SplitStake { stake_account_pubkey: Pubkey, stake_authority: SignerIndex, sign_only: bool, blockhash_query: BlockhashQuery, nonce_account: Option, nonce_authority: SignerIndex, split_stake_account: SignerIndex, seed: Option, lamports: u64, fee_payer: SignerIndex, }, ShowStakeHistory { use_lamports_unit: bool, }, ShowStakeAccount { pubkey: Pubkey, use_lamports_unit: bool, }, StakeAuthorize { stake_account_pubkey: Pubkey, new_authorizations: Vec<(StakeAuthorize, Pubkey, SignerIndex)>, sign_only: bool, blockhash_query: BlockhashQuery, nonce_account: Option, nonce_authority: SignerIndex, fee_payer: SignerIndex, }, StakeSetLockup { stake_account_pubkey: Pubkey, lockup: LockupArgs, custodian: SignerIndex, sign_only: bool, blockhash_query: BlockhashQuery, nonce_account: Option, nonce_authority: SignerIndex, fee_payer: SignerIndex, }, WithdrawStake { stake_account_pubkey: Pubkey, destination_account_pubkey: Pubkey, lamports: u64, withdraw_authority: SignerIndex, custodian: Option, sign_only: bool, blockhash_query: BlockhashQuery, nonce_account: Option, nonce_authority: SignerIndex, fee_payer: SignerIndex, }, // Validator Info Commands GetValidatorInfo(Option), SetValidatorInfo { validator_info: Value, force_keybase: bool, info_pubkey: Option, }, // Vote Commands CreateVoteAccount { seed: Option, identity_account: SignerIndex, authorized_voter: Option, authorized_withdrawer: Option, commission: u8, }, ShowVoteAccount { pubkey: Pubkey, use_lamports_unit: bool, commitment_config: CommitmentConfig, }, WithdrawFromVoteAccount { vote_account_pubkey: Pubkey, destination_account_pubkey: Pubkey, withdraw_authority: SignerIndex, lamports: u64, }, VoteAuthorize { vote_account_pubkey: Pubkey, new_authorized_pubkey: Pubkey, vote_authorize: VoteAuthorize, }, VoteUpdateValidator { vote_account_pubkey: Pubkey, new_identity_account: SignerIndex, }, // Wallet Commands Address, Airdrop { faucet_host: Option, faucet_port: u16, pubkey: Option, lamports: u64, }, Balance { pubkey: Option, use_lamports_unit: bool, commitment_config: CommitmentConfig, }, Cancel(Pubkey), Confirm(Signature), DecodeTransaction(Transaction), Pay(PayCommand), ResolveSigner(Option), ShowAccount { pubkey: Pubkey, output_file: Option, use_lamports_unit: bool, }, TimeElapsed(Pubkey, Pubkey, DateTime), // TimeElapsed(to, process_id, timestamp) Transfer { amount: SpendAmount, to: Pubkey, from: SignerIndex, sign_only: bool, no_wait: bool, blockhash_query: BlockhashQuery, nonce_account: Option, nonce_authority: SignerIndex, fee_payer: SignerIndex, }, Witness(Pubkey, Pubkey), // Witness(to, process_id) } #[derive(Debug, PartialEq)] pub struct CliCommandInfo { pub command: CliCommand, pub signers: CliSigners, } #[derive(Debug, Error)] pub enum CliError { #[error("bad parameter: {0}")] BadParameter(String), #[error(transparent)] ClientError(#[from] ClientError), #[error("command not recognized: {0}")] CommandNotRecognized(String), #[error("insufficient funds for fee ({0} SOL)")] InsufficientFundsForFee(f64), #[error("insufficient funds for spend ({0} SOL)")] InsufficientFundsForSpend(f64), #[error("insufficient funds for spend ({0} SOL) and fee ({1} SOL)")] InsufficientFundsForSpendAndFee(f64, f64), #[error(transparent)] InvalidNonce(CliNonceError), #[error("dynamic program error: {0}")] DynamicProgramError(String), #[error("rpc request error: {0}")] RpcRequestError(String), #[error("keypair file not found: {0}")] KeypairFileNotFound(String), } impl From> for CliError { fn from(error: Box) -> Self { CliError::DynamicProgramError(error.to_string()) } } impl From for CliError { fn from(error: CliNonceError) -> Self { match error { CliNonceError::Client(client_error) => Self::RpcRequestError(client_error), _ => Self::InvalidNonce(error), } } } pub enum SettingType { Explicit, Computed, SystemDefault, } pub struct CliConfig<'a> { pub command: CliCommand, pub json_rpc_url: String, pub websocket_url: String, pub signers: Vec<&'a dyn Signer>, pub keypair_path: String, pub rpc_client: Option, pub verbose: bool, pub output_format: OutputFormat, } impl CliConfig<'_> { fn default_keypair_path() -> String { solana_cli_config::Config::default().keypair_path } fn default_json_rpc_url() -> String { solana_cli_config::Config::default().json_rpc_url } fn default_websocket_url() -> String { solana_cli_config::Config::default().websocket_url } fn first_nonempty_setting( settings: std::vec::Vec<(SettingType, String)>, ) -> (SettingType, String) { settings .into_iter() .find(|(_, value)| value != "") .expect("no nonempty setting") } pub fn compute_websocket_url_setting( websocket_cmd_url: &str, websocket_cfg_url: &str, json_rpc_cmd_url: &str, json_rpc_cfg_url: &str, ) -> (SettingType, String) { Self::first_nonempty_setting(vec![ (SettingType::Explicit, websocket_cmd_url.to_string()), (SettingType::Explicit, websocket_cfg_url.to_string()), ( SettingType::Computed, solana_cli_config::Config::compute_websocket_url(json_rpc_cmd_url), ), ( SettingType::Computed, solana_cli_config::Config::compute_websocket_url(json_rpc_cfg_url), ), (SettingType::SystemDefault, Self::default_websocket_url()), ]) } pub fn compute_json_rpc_url_setting( json_rpc_cmd_url: &str, json_rpc_cfg_url: &str, ) -> (SettingType, String) { Self::first_nonempty_setting(vec![ (SettingType::Explicit, json_rpc_cmd_url.to_string()), (SettingType::Explicit, json_rpc_cfg_url.to_string()), (SettingType::SystemDefault, Self::default_json_rpc_url()), ]) } pub fn compute_keypair_path_setting( keypair_cmd_path: &str, keypair_cfg_path: &str, ) -> (SettingType, String) { Self::first_nonempty_setting(vec![ (SettingType::Explicit, keypair_cmd_path.to_string()), (SettingType::Explicit, keypair_cfg_path.to_string()), (SettingType::SystemDefault, Self::default_keypair_path()), ]) } pub(crate) fn pubkey(&self) -> Result { if !self.signers.is_empty() { self.signers[0].try_pubkey() } else { Err(SignerError::Custom( "Default keypair must be set if pubkey arg not provided".to_string(), )) } } } impl Default for CliConfig<'_> { fn default() -> CliConfig<'static> { CliConfig { command: CliCommand::Balance { pubkey: Some(Pubkey::default()), use_lamports_unit: false, commitment_config: CommitmentConfig::default(), }, json_rpc_url: Self::default_json_rpc_url(), websocket_url: Self::default_websocket_url(), signers: Vec::new(), keypair_path: Self::default_keypair_path(), rpc_client: None, verbose: false, output_format: OutputFormat::Display, } } } pub fn parse_command( matches: &ArgMatches<'_>, default_signer_path: &str, wallet_manager: &mut Option>, ) -> Result> { let response = match matches.subcommand() { // Cluster Query Commands ("catchup", Some(matches)) => parse_catchup(matches, wallet_manager), ("cluster-date", Some(_matches)) => Ok(CliCommandInfo { command: CliCommand::ClusterDate, signers: vec![], }), ("cluster-version", Some(_matches)) => Ok(CliCommandInfo { command: CliCommand::ClusterVersion, signers: vec![], }), ("create-address-with-seed", Some(matches)) => { parse_create_address_with_seed(matches, default_signer_path, wallet_manager) } ("fees", Some(_matches)) => Ok(CliCommandInfo { command: CliCommand::Fees, signers: vec![], }), ("block-time", Some(matches)) => parse_get_block_time(matches), ("epoch-info", Some(matches)) => parse_get_epoch_info(matches), ("genesis-hash", Some(_matches)) => Ok(CliCommandInfo { command: CliCommand::GetGenesisHash, signers: vec![], }), ("epoch", Some(matches)) => parse_get_epoch(matches), ("slot", Some(matches)) => parse_get_slot(matches), ("largest-accounts", Some(matches)) => parse_largest_accounts(matches), ("supply", Some(matches)) => parse_supply(matches), ("total-supply", Some(matches)) => parse_total_supply(matches), ("transaction-count", Some(matches)) => parse_get_transaction_count(matches), ("leader-schedule", Some(_matches)) => Ok(CliCommandInfo { command: CliCommand::LeaderSchedule, signers: vec![], }), ("ping", Some(matches)) => parse_cluster_ping(matches, default_signer_path, wallet_manager), ("live-slots", Some(_matches)) => Ok(CliCommandInfo { command: CliCommand::LiveSlots, signers: vec![], }), ("block-production", Some(matches)) => parse_show_block_production(matches), ("gossip", Some(_matches)) => Ok(CliCommandInfo { command: CliCommand::ShowGossip, signers: vec![], }), ("stakes", Some(matches)) => parse_show_stakes(matches, wallet_manager), ("validators", Some(matches)) => parse_show_validators(matches), ("transaction-history", Some(matches)) => { parse_transaction_history(matches, wallet_manager) } // Nonce Commands ("authorize-nonce-account", Some(matches)) => { parse_authorize_nonce_account(matches, default_signer_path, wallet_manager) } ("create-nonce-account", Some(matches)) => { parse_nonce_create_account(matches, default_signer_path, wallet_manager) } ("nonce", Some(matches)) => parse_get_nonce(matches, wallet_manager), ("new-nonce", Some(matches)) => { parse_new_nonce(matches, default_signer_path, wallet_manager) } ("nonce-account", Some(matches)) => parse_show_nonce_account(matches, wallet_manager), ("withdraw-from-nonce-account", Some(matches)) => { parse_withdraw_from_nonce_account(matches, default_signer_path, wallet_manager) } // Program Deployment ("deploy", Some(matches)) => Ok(CliCommandInfo { command: CliCommand::Deploy(matches.value_of("program_location").unwrap().to_string()), signers: vec![signer_from_path( matches, default_signer_path, "keypair", wallet_manager, )?], }), // Stake Commands ("create-stake-account", Some(matches)) => { parse_stake_create_account(matches, default_signer_path, wallet_manager) } ("delegate-stake", Some(matches)) => { parse_stake_delegate_stake(matches, default_signer_path, wallet_manager) } ("withdraw-stake", Some(matches)) => { parse_stake_withdraw_stake(matches, default_signer_path, wallet_manager) } ("deactivate-stake", Some(matches)) => { parse_stake_deactivate_stake(matches, default_signer_path, wallet_manager) } ("split-stake", Some(matches)) => { parse_split_stake(matches, default_signer_path, wallet_manager) } ("stake-authorize", Some(matches)) => { parse_stake_authorize(matches, default_signer_path, wallet_manager) } ("stake-set-lockup", Some(matches)) => { parse_stake_set_lockup(matches, default_signer_path, wallet_manager) } ("stake-account", Some(matches)) => parse_show_stake_account(matches, wallet_manager), ("stake-history", Some(matches)) => parse_show_stake_history(matches), // Validator Info Commands ("validator-info", Some(matches)) => match matches.subcommand() { ("publish", Some(matches)) => { parse_validator_info_command(matches, default_signer_path, wallet_manager) } ("get", Some(matches)) => parse_get_validator_info_command(matches), _ => unreachable!(), }, // Vote Commands ("create-vote-account", Some(matches)) => { parse_create_vote_account(matches, default_signer_path, wallet_manager) } ("vote-update-validator", Some(matches)) => { parse_vote_update_validator(matches, default_signer_path, wallet_manager) } ("vote-authorize-voter", Some(matches)) => parse_vote_authorize( matches, default_signer_path, wallet_manager, VoteAuthorize::Voter, ), ("vote-authorize-withdrawer", Some(matches)) => parse_vote_authorize( matches, default_signer_path, wallet_manager, VoteAuthorize::Withdrawer, ), ("vote-account", Some(matches)) => parse_vote_get_account_command(matches, wallet_manager), ("withdraw-from-vote-account", Some(matches)) => { parse_withdraw_from_vote_account(matches, default_signer_path, wallet_manager) } // Wallet Commands ("address", Some(matches)) => Ok(CliCommandInfo { command: CliCommand::Address, signers: vec![signer_from_path( matches, default_signer_path, "keypair", wallet_manager, )?], }), ("airdrop", Some(matches)) => { let faucet_port = matches .value_of("faucet_port") .unwrap() .parse() .or_else(|err| { Err(CliError::BadParameter(format!( "Invalid faucet port: {}", err ))) })?; let faucet_host = if let Some(faucet_host) = matches.value_of("faucet_host") { Some(solana_net_utils::parse_host(faucet_host).or_else(|err| { Err(CliError::BadParameter(format!( "Invalid faucet host: {}", err ))) })?) } else { None }; let pubkey = pubkey_of_signer(matches, "to", wallet_manager)?; let signers = if pubkey.is_some() { vec![] } else { vec![signer_from_path( matches, default_signer_path, "keypair", wallet_manager, )?] }; let lamports = lamports_of_sol(matches, "amount").unwrap(); Ok(CliCommandInfo { command: CliCommand::Airdrop { faucet_host, faucet_port, pubkey, lamports, }, signers, }) } ("balance", Some(matches)) => { let pubkey = pubkey_of_signer(matches, "pubkey", wallet_manager)?; let commitment_config = commitment_of(matches, COMMITMENT_ARG.long).unwrap(); let signers = if pubkey.is_some() { vec![] } else { vec![signer_from_path( matches, default_signer_path, "keypair", wallet_manager, )?] }; Ok(CliCommandInfo { command: CliCommand::Balance { pubkey, use_lamports_unit: matches.is_present("lamports"), commitment_config, }, signers, }) } ("cancel", Some(matches)) => { let process_id = value_of(matches, "process_id").unwrap(); let default_signer = signer_from_path(matches, default_signer_path, "keypair", wallet_manager)?; Ok(CliCommandInfo { command: CliCommand::Cancel(process_id), signers: vec![default_signer], }) } ("confirm", Some(matches)) => match matches.value_of("signature").unwrap().parse() { Ok(signature) => Ok(CliCommandInfo { command: CliCommand::Confirm(signature), signers: vec![], }), _ => Err(CliError::BadParameter("Invalid signature".to_string())), }, ("decode-transaction", Some(matches)) => { let encoded_transaction = EncodedTransaction::Binary( matches.value_of("base58_transaction").unwrap().to_string(), ); if let Some(transaction) = encoded_transaction.decode() { Ok(CliCommandInfo { command: CliCommand::DecodeTransaction(transaction), signers: vec![], }) } else { Err(CliError::BadParameter( "Unable to decode transaction".to_string(), )) } } ("pay", Some(matches)) => { let amount = SpendAmount::new_from_matches(matches, "amount"); let to = pubkey_of_signer(matches, "to", wallet_manager)?.unwrap(); let timestamp = if matches.is_present("timestamp") { // Parse input for serde_json let date_string = if !matches.value_of("timestamp").unwrap().contains('Z') { format!("\"{}Z\"", matches.value_of("timestamp").unwrap()) } else { format!("\"{}\"", matches.value_of("timestamp").unwrap()) }; Some(serde_json::from_str(&date_string)?) } else { None }; let timestamp_pubkey = value_of(matches, "timestamp_pubkey"); let witnesses = values_of(matches, "witness"); let cancelable = matches.is_present("cancelable"); let sign_only = matches.is_present(SIGN_ONLY_ARG.name); let blockhash_query = BlockhashQuery::new_from_matches(matches); let nonce_account = pubkey_of_signer(matches, NONCE_ARG.name, wallet_manager)?; let (nonce_authority, nonce_authority_pubkey) = signer_of(matches, NONCE_AUTHORITY_ARG.name, wallet_manager)?; let payer_provided = None; let mut bulk_signers = vec![payer_provided]; if nonce_account.is_some() { bulk_signers.push(nonce_authority); } let signer_info = generate_unique_signers( bulk_signers, matches, default_signer_path, wallet_manager, )?; Ok(CliCommandInfo { command: CliCommand::Pay(PayCommand { amount, to, timestamp, timestamp_pubkey, witnesses, cancelable, sign_only, blockhash_query, nonce_account, nonce_authority: signer_info.index_of(nonce_authority_pubkey).unwrap(), }), signers: signer_info.signers, }) } ("account", Some(matches)) => { let account_pubkey = pubkey_of_signer(matches, "account_pubkey", wallet_manager)?.unwrap(); let output_file = matches.value_of("output_file"); let use_lamports_unit = matches.is_present("lamports"); Ok(CliCommandInfo { command: CliCommand::ShowAccount { pubkey: account_pubkey, output_file: output_file.map(ToString::to_string), use_lamports_unit, }, signers: vec![], }) } ("resolve-signer", Some(matches)) => { let signer_path = resolve_signer(matches, "signer", wallet_manager)?; Ok(CliCommandInfo { command: CliCommand::ResolveSigner(signer_path), signers: vec![], }) } ("send-signature", Some(matches)) => { let to = value_of(matches, "to").unwrap(); let process_id = value_of(matches, "process_id").unwrap(); let default_signer = signer_from_path(matches, default_signer_path, "keypair", wallet_manager)?; Ok(CliCommandInfo { command: CliCommand::Witness(to, process_id), signers: vec![default_signer], }) } ("send-timestamp", Some(matches)) => { let to = value_of(matches, "to").unwrap(); let process_id = value_of(matches, "process_id").unwrap(); let dt = if matches.is_present("datetime") { // Parse input for serde_json let date_string = if !matches.value_of("datetime").unwrap().contains('Z') { format!("\"{}Z\"", matches.value_of("datetime").unwrap()) } else { format!("\"{}\"", matches.value_of("datetime").unwrap()) }; serde_json::from_str(&date_string)? } else { Utc::now() }; let default_signer = signer_from_path(matches, default_signer_path, "keypair", wallet_manager)?; Ok(CliCommandInfo { command: CliCommand::TimeElapsed(to, process_id, dt), signers: vec![default_signer], }) } ("transfer", Some(matches)) => { let amount = SpendAmount::new_from_matches(matches, "amount"); let to = pubkey_of_signer(matches, "to", wallet_manager)?.unwrap(); let sign_only = matches.is_present(SIGN_ONLY_ARG.name); let no_wait = matches.is_present("no_wait"); let blockhash_query = BlockhashQuery::new_from_matches(matches); let nonce_account = pubkey_of_signer(matches, NONCE_ARG.name, wallet_manager)?; let (nonce_authority, nonce_authority_pubkey) = signer_of(matches, NONCE_AUTHORITY_ARG.name, wallet_manager)?; let (fee_payer, fee_payer_pubkey) = signer_of(matches, FEE_PAYER_ARG.name, wallet_manager)?; let (from, from_pubkey) = signer_of(matches, "from", wallet_manager)?; let mut bulk_signers = vec![fee_payer, from]; if nonce_account.is_some() { bulk_signers.push(nonce_authority); } let signer_info = generate_unique_signers( bulk_signers, matches, default_signer_path, wallet_manager, )?; Ok(CliCommandInfo { command: CliCommand::Transfer { amount, to, sign_only, no_wait, blockhash_query, nonce_account, nonce_authority: signer_info.index_of(nonce_authority_pubkey).unwrap(), fee_payer: signer_info.index_of(fee_payer_pubkey).unwrap(), from: signer_info.index_of(from_pubkey).unwrap(), }, signers: signer_info.signers, }) } // ("", None) => { eprintln!("{}", matches.usage()); Err(CliError::CommandNotRecognized( "no subcommand given".to_string(), )) } _ => unreachable!(), }?; Ok(response) } pub type ProcessResult = Result>; pub fn get_blockhash_and_fee_calculator( rpc_client: &RpcClient, sign_only: bool, blockhash: Option, ) -> Result<(Hash, FeeCalculator), Box> { Ok(if let Some(blockhash) = blockhash { if sign_only { (blockhash, FeeCalculator::default()) } else { (blockhash, rpc_client.get_recent_blockhash()?.1) } } else { rpc_client.get_recent_blockhash()? }) } pub fn return_signers(tx: &Transaction, config: &CliConfig) -> ProcessResult { let verify_results = tx.verify_with_results(); let mut signers = Vec::new(); let mut absent = Vec::new(); let mut bad_sig = Vec::new(); tx.signatures .iter() .zip(tx.message.account_keys.iter()) .zip(verify_results.into_iter()) .for_each(|((sig, key), res)| { if res { signers.push(format!("{}={}", key, sig)) } else if *sig == Signature::default() { absent.push(key.to_string()); } else { bad_sig.push(key.to_string()); } }); let cli_command = CliSignOnlyData { blockhash: tx.message.recent_blockhash.to_string(), signers, absent, bad_sig, }; Ok(config.output_format.formatted_string(&cli_command)) } pub fn parse_create_address_with_seed( matches: &ArgMatches<'_>, default_signer_path: &str, wallet_manager: &mut Option>, ) -> Result { let from_pubkey = pubkey_of_signer(matches, "from", wallet_manager)?; let signers = if from_pubkey.is_some() { vec![] } else { vec![signer_from_path( matches, default_signer_path, "keypair", wallet_manager, )?] }; let program_id = match matches.value_of("program_id").unwrap() { "NONCE" => system_program::id(), "STAKE" => solana_stake_program::id(), "VOTE" => solana_vote_program::id(), _ => pubkey_of(matches, "program_id").unwrap(), }; let seed = matches.value_of("seed").unwrap().to_string(); if seed.len() > MAX_SEED_LEN { return Err(CliError::BadParameter( "Address seed must not be longer than 32 bytes".to_string(), )); } Ok(CliCommandInfo { command: CliCommand::CreateAddressWithSeed { from_pubkey, seed, program_id, }, signers, }) } fn process_create_address_with_seed( config: &CliConfig, from_pubkey: Option<&Pubkey>, seed: &str, program_id: &Pubkey, ) -> ProcessResult { let from_pubkey = if let Some(pubkey) = from_pubkey { *pubkey } else { config.pubkey()? }; let address = Pubkey::create_with_seed(&from_pubkey, seed, program_id)?; Ok(address.to_string()) } fn process_airdrop( rpc_client: &RpcClient, config: &CliConfig, faucet_addr: &SocketAddr, pubkey: &Option, lamports: u64, ) -> ProcessResult { let pubkey = if let Some(pubkey) = pubkey { *pubkey } else { config.pubkey()? }; println!( "Requesting airdrop of {} from {}", build_balance_message(lamports, false, true), faucet_addr ); request_and_confirm_airdrop(&rpc_client, faucet_addr, &pubkey, lamports, &config)?; let current_balance = rpc_client.get_balance(&pubkey)?; Ok(build_balance_message(current_balance, false, true)) } fn process_balance( rpc_client: &RpcClient, config: &CliConfig, pubkey: &Option, use_lamports_unit: bool, commitment_config: CommitmentConfig, ) -> ProcessResult { let pubkey = if let Some(pubkey) = pubkey { *pubkey } else { config.pubkey()? }; let balance = rpc_client .get_balance_with_commitment(&pubkey, commitment_config)? .value; Ok(build_balance_message(balance, use_lamports_unit, true)) } fn process_confirm( rpc_client: &RpcClient, config: &CliConfig, signature: &Signature, ) -> ProcessResult { match rpc_client.get_signature_status_with_commitment_and_history( &signature, CommitmentConfig::max(), true, ) { Ok(status) => { if let Some(transaction_status) = status { if config.verbose { match rpc_client .get_confirmed_transaction(signature, TransactionEncoding::Binary) { Ok(confirmed_transaction) => { println!( "\nTransaction executed in slot {}:", confirmed_transaction.slot ); crate::display::println_transaction( &confirmed_transaction .transaction .transaction .decode() .expect("Successful decode"), &confirmed_transaction.transaction.meta, " ", ); } Err(err) => { println!("Unable to get confirmed transaction details: {}", err) } } println!(); } match transaction_status { Ok(_) => Ok("Confirmed".to_string()), Err(err) => Ok(format!("Transaction failed: {}", err)), } } else { Ok("Not found".to_string()) } } Err(err) => Err(CliError::RpcRequestError(format!("Unable to confirm: {}", err)).into()), } } fn process_decode_transaction(transaction: &Transaction) -> ProcessResult { crate::display::println_transaction(transaction, &None, ""); Ok("".to_string()) } fn process_show_account( rpc_client: &RpcClient, config: &CliConfig, account_pubkey: &Pubkey, output_file: &Option, use_lamports_unit: bool, ) -> ProcessResult { let account = rpc_client.get_account(account_pubkey)?; let data = account.data.clone(); let cli_account = CliAccount { keyed_account: RpcKeyedAccount { pubkey: account_pubkey.to_string(), account: RpcAccount::encode(account), }, use_lamports_unit, }; let mut account_string = config.output_format.formatted_string(&cli_account); if config.output_format == OutputFormat::Display { if let Some(output_file) = output_file { let mut f = File::create(output_file)?; f.write_all(&data)?; writeln!(&mut account_string)?; writeln!(&mut account_string, "Wrote account data to {}", output_file)?; } else if !data.is_empty() { use pretty_hex::*; writeln!(&mut account_string, "{:?}", data.hex_dump())?; } } Ok(account_string) } fn process_deploy( rpc_client: &RpcClient, config: &CliConfig, program_location: &str, ) -> ProcessResult { let program_id = Keypair::new(); let mut file = File::open(program_location).map_err(|err| { CliError::DynamicProgramError(format!("Unable to open program file: {}", err)) })?; let mut program_data = Vec::new(); file.read_to_end(&mut program_data).map_err(|err| { CliError::DynamicProgramError(format!("Unable to read program file: {}", err)) })?; // Build transactions to calculate fees let mut messages: Vec<&Message> = Vec::new(); let (blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; let minimum_balance = rpc_client.get_minimum_balance_for_rent_exemption(program_data.len())?; let ix = system_instruction::create_account( &config.signers[0].pubkey(), &program_id.pubkey(), minimum_balance.max(1), program_data.len() as u64, &bpf_loader::id(), ); let message = Message::new(&[ix]); let mut create_account_tx = Transaction::new_unsigned(message); create_account_tx.try_sign(&[config.signers[0], &program_id], blockhash)?; messages.push(&create_account_tx.message); let signers = [config.signers[0], &program_id]; let mut write_transactions = vec![]; for (chunk, i) in program_data.chunks(DATA_CHUNK_SIZE).zip(0..) { let instruction = loader_instruction::write( &program_id.pubkey(), &bpf_loader::id(), (i * DATA_CHUNK_SIZE) as u32, chunk.to_vec(), ); let message = Message::new_with_payer(&[instruction], Some(&signers[0].pubkey())); let mut tx = Transaction::new_unsigned(message); tx.try_sign(&signers, blockhash)?; write_transactions.push(tx); } for transaction in write_transactions.iter() { messages.push(&transaction.message); } let instruction = loader_instruction::finalize(&program_id.pubkey(), &bpf_loader::id()); let message = Message::new_with_payer(&[instruction], Some(&signers[0].pubkey())); let mut finalize_tx = Transaction::new_unsigned(message); finalize_tx.try_sign(&signers, blockhash)?; messages.push(&finalize_tx.message); check_account_for_multiple_fees( rpc_client, &config.signers[0].pubkey(), &fee_calculator, &messages, )?; trace!("Creating program account"); let result = rpc_client.send_and_confirm_transaction_with_spinner(&create_account_tx); log_instruction_custom_error::(result, &config).map_err(|_| { CliError::DynamicProgramError("Program account allocation failed".to_string()) })?; trace!("Writing program data"); rpc_client .send_and_confirm_transactions_with_spinner(write_transactions, &signers) .map_err(|_| { CliError::DynamicProgramError("Data writes to program account failed".to_string()) })?; trace!("Finalizing program account"); rpc_client .send_and_confirm_transaction_with_spinner(&finalize_tx) .map_err(|e| { CliError::DynamicProgramError(format!("Finalizing program account failed: {}", e)) })?; Ok(json!({ "programId": format!("{}", program_id.pubkey()), }) .to_string()) } #[allow(clippy::too_many_arguments)] fn process_pay( rpc_client: &RpcClient, config: &CliConfig, amount: SpendAmount, to: &Pubkey, timestamp: Option>, timestamp_pubkey: Option, witnesses: &Option>, cancelable: bool, sign_only: bool, blockhash_query: &BlockhashQuery, nonce_account: Option, nonce_authority: SignerIndex, ) -> ProcessResult { check_unique_pubkeys( (&config.signers[0].pubkey(), "cli keypair".to_string()), (to, "to".to_string()), )?; let (blockhash, fee_calculator) = blockhash_query.get_blockhash_and_fee_calculator(rpc_client)?; let cancelable = if cancelable { Some(config.signers[0].pubkey()) } else { None }; if timestamp == None && *witnesses == None { let nonce_authority = config.signers[nonce_authority]; let build_message = |lamports| { let ix = system_instruction::transfer(&config.signers[0].pubkey(), to, lamports); if let Some(nonce_account) = &nonce_account { Message::new_with_nonce(vec![ix], None, nonce_account, &nonce_authority.pubkey()) } else { Message::new(&[ix]) } }; let (message, _) = resolve_spend_tx_and_check_account_balance( rpc_client, sign_only, amount, &fee_calculator, &config.signers[0].pubkey(), build_message, )?; let mut tx = Transaction::new_unsigned(message); if sign_only { tx.try_partial_sign(&config.signers, blockhash)?; return_signers(&tx, &config) } else { if let Some(nonce_account) = &nonce_account { let nonce_account = rpc_client.get_account(nonce_account)?; check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &blockhash)?; } tx.try_sign(&config.signers, blockhash)?; let result = rpc_client.send_and_confirm_transaction_with_spinner(&tx); log_instruction_custom_error::(result, &config) } } else if *witnesses == None { let dt = timestamp.unwrap(); let dt_pubkey = match timestamp_pubkey { Some(pubkey) => pubkey, None => config.signers[0].pubkey(), }; let contract_state = Keypair::new(); let build_message = |lamports| { // Initializing contract let ixs = budget_instruction::on_date( &config.signers[0].pubkey(), to, &contract_state.pubkey(), dt, &dt_pubkey, cancelable, lamports, ); Message::new(&ixs) }; let (message, _) = resolve_spend_tx_and_check_account_balance( rpc_client, sign_only, amount, &fee_calculator, &config.signers[0].pubkey(), build_message, )?; let mut tx = Transaction::new_unsigned(message); if sign_only { tx.try_partial_sign(&[config.signers[0], &contract_state], blockhash)?; return_signers(&tx, &config) } else { tx.try_sign(&[config.signers[0], &contract_state], blockhash)?; let result = rpc_client.send_and_confirm_transaction_with_spinner(&tx); let signature = log_instruction_custom_error::(result, &config)?; Ok(json!({ "signature": signature, "processId": format!("{}", contract_state.pubkey()), }) .to_string()) } } else if timestamp == None { let witness = if let Some(ref witness_vec) = *witnesses { witness_vec[0] } else { return Err(CliError::BadParameter( "Could not parse required signature pubkey(s)".to_string(), ) .into()); }; let contract_state = Keypair::new(); let build_message = |lamports| { // Initializing contract let ixs = budget_instruction::when_signed( &config.signers[0].pubkey(), to, &contract_state.pubkey(), &witness, cancelable, lamports, ); Message::new(&ixs) }; let (message, _) = resolve_spend_tx_and_check_account_balance( rpc_client, sign_only, amount, &fee_calculator, &config.signers[0].pubkey(), build_message, )?; let mut tx = Transaction::new_unsigned(message); if sign_only { tx.try_partial_sign(&[config.signers[0], &contract_state], blockhash)?; return_signers(&tx, &config) } else { tx.try_sign(&[config.signers[0], &contract_state], blockhash)?; let result = rpc_client.send_and_confirm_transaction_with_spinner(&tx); let signature = log_instruction_custom_error::(result, &config)?; Ok(json!({ "signature": signature, "processId": format!("{}", contract_state.pubkey()), }) .to_string()) } } else { Ok("Combo transactions not yet handled".to_string()) } } fn process_cancel(rpc_client: &RpcClient, config: &CliConfig, pubkey: &Pubkey) -> ProcessResult { let (blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; let ix = budget_instruction::apply_signature( &config.signers[0].pubkey(), pubkey, &config.signers[0].pubkey(), ); let message = Message::new(&[ix]); let mut tx = Transaction::new_unsigned(message); tx.try_sign(&config.signers, blockhash)?; check_account_for_fee( rpc_client, &config.signers[0].pubkey(), &fee_calculator, &tx.message, )?; let result = rpc_client.send_and_confirm_transaction_with_spinner(&tx); log_instruction_custom_error::(result, &config) } fn process_time_elapsed( rpc_client: &RpcClient, config: &CliConfig, to: &Pubkey, pubkey: &Pubkey, dt: DateTime, ) -> ProcessResult { let (blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; let ix = budget_instruction::apply_timestamp(&config.signers[0].pubkey(), pubkey, to, dt); let message = Message::new(&[ix]); let mut tx = Transaction::new_unsigned(message); tx.try_sign(&config.signers, blockhash)?; check_account_for_fee( rpc_client, &config.signers[0].pubkey(), &fee_calculator, &tx.message, )?; let result = rpc_client.send_and_confirm_transaction_with_spinner(&tx); log_instruction_custom_error::(result, &config) } #[allow(clippy::too_many_arguments)] fn process_transfer( rpc_client: &RpcClient, config: &CliConfig, amount: SpendAmount, to: &Pubkey, from: SignerIndex, sign_only: bool, no_wait: bool, blockhash_query: &BlockhashQuery, nonce_account: Option<&Pubkey>, nonce_authority: SignerIndex, fee_payer: SignerIndex, ) -> ProcessResult { let from = config.signers[from]; check_unique_pubkeys( (&from.pubkey(), "cli keypair".to_string()), (to, "to".to_string()), )?; let (recent_blockhash, fee_calculator) = blockhash_query.get_blockhash_and_fee_calculator(rpc_client)?; let nonce_authority = config.signers[nonce_authority]; let fee_payer = config.signers[fee_payer]; let build_message = |lamports| { let ixs = vec![system_instruction::transfer(&from.pubkey(), to, lamports)]; if let Some(nonce_account) = &nonce_account { Message::new_with_nonce( ixs, Some(&fee_payer.pubkey()), nonce_account, &nonce_authority.pubkey(), ) } else { Message::new_with_payer(&ixs, Some(&fee_payer.pubkey())) } }; let (message, _) = resolve_spend_tx_and_check_account_balances( rpc_client, sign_only, amount, &fee_calculator, &from.pubkey(), &fee_payer.pubkey(), build_message, )?; let mut tx = Transaction::new_unsigned(message); if sign_only { tx.try_partial_sign(&config.signers, recent_blockhash)?; return_signers(&tx, &config) } else { if let Some(nonce_account) = &nonce_account { let nonce_account = rpc_client.get_account(nonce_account)?; check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?; } tx.try_sign(&config.signers, recent_blockhash)?; if let Some(nonce_account) = &nonce_account { let nonce_account = rpc_client.get_account(nonce_account)?; check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?; } let result = if no_wait { rpc_client.send_transaction(&tx) } else { rpc_client.send_and_confirm_transaction_with_spinner(&tx) }; log_instruction_custom_error::(result, &config) } } fn process_witness( rpc_client: &RpcClient, config: &CliConfig, to: &Pubkey, pubkey: &Pubkey, ) -> ProcessResult { let (blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; let ix = budget_instruction::apply_signature(&config.signers[0].pubkey(), pubkey, to); let message = Message::new(&[ix]); let mut tx = Transaction::new_unsigned(message); tx.try_sign(&config.signers, blockhash)?; check_account_for_fee( rpc_client, &config.signers[0].pubkey(), &fee_calculator, &tx.message, )?; let result = rpc_client.send_and_confirm_transaction_with_spinner(&tx); log_instruction_custom_error::(result, &config) } pub fn process_command(config: &CliConfig) -> ProcessResult { if config.verbose && config.output_format == OutputFormat::Display { println_name_value("RPC URL:", &config.json_rpc_url); println_name_value("Default Signer Path:", &config.keypair_path); if config.keypair_path.starts_with("usb://") { println_name_value("Pubkey:", &format!("{:?}", config.pubkey()?)); } } let mut _rpc_client; let rpc_client = if config.rpc_client.is_none() { _rpc_client = RpcClient::new(config.json_rpc_url.to_string()); &_rpc_client } else { // Primarily for testing config.rpc_client.as_ref().unwrap() }; match &config.command { // Cluster Query Commands // Get address of this client CliCommand::Address => Ok(format!("{}", config.pubkey()?)), // Return software version of solana-cli and cluster entrypoint node CliCommand::Catchup { node_pubkey, node_json_rpc_url, commitment_config, follow, } => process_catchup( &rpc_client, node_pubkey, node_json_rpc_url, *commitment_config, *follow, ), CliCommand::ClusterDate => process_cluster_date(&rpc_client, config), CliCommand::ClusterVersion => process_cluster_version(&rpc_client), CliCommand::CreateAddressWithSeed { from_pubkey, seed, program_id, } => process_create_address_with_seed(config, from_pubkey.as_ref(), &seed, &program_id), CliCommand::Fees => process_fees(&rpc_client), CliCommand::GetBlockTime { slot } => process_get_block_time(&rpc_client, config, *slot), CliCommand::GetGenesisHash => process_get_genesis_hash(&rpc_client), CliCommand::GetEpochInfo { commitment_config } => { process_get_epoch_info(&rpc_client, config, *commitment_config) } CliCommand::GetEpoch { commitment_config } => { process_get_epoch(&rpc_client, *commitment_config) } CliCommand::GetSlot { commitment_config } => { process_get_slot(&rpc_client, *commitment_config) } CliCommand::LargestAccounts { commitment_config, filter, } => process_largest_accounts(&rpc_client, config, *commitment_config, filter.clone()), CliCommand::Supply { commitment_config, print_accounts, } => process_supply(&rpc_client, config, *commitment_config, *print_accounts), CliCommand::TotalSupply { commitment_config } => { process_total_supply(&rpc_client, *commitment_config) } CliCommand::GetTransactionCount { commitment_config } => { process_get_transaction_count(&rpc_client, *commitment_config) } CliCommand::LeaderSchedule => process_leader_schedule(&rpc_client), CliCommand::LiveSlots => process_live_slots(&config.websocket_url), CliCommand::Ping { lamports, interval, count, timeout, commitment_config, } => process_ping( &rpc_client, config, *lamports, interval, count, timeout, *commitment_config, ), CliCommand::ShowBlockProduction { epoch, slot_limit } => { process_show_block_production(&rpc_client, config, *epoch, *slot_limit) } CliCommand::ShowGossip => process_show_gossip(&rpc_client), CliCommand::ShowStakes { use_lamports_unit, vote_account_pubkeys, } => process_show_stakes( &rpc_client, config, *use_lamports_unit, vote_account_pubkeys.as_deref(), ), CliCommand::ShowValidators { use_lamports_unit, commitment_config, } => process_show_validators(&rpc_client, config, *use_lamports_unit, *commitment_config), CliCommand::TransactionHistory { address, end_slot, slot_limit, } => process_transaction_history(&rpc_client, address, *end_slot, *slot_limit), // Nonce Commands // Assign authority to nonce account CliCommand::AuthorizeNonceAccount { nonce_account, nonce_authority, new_authority, } => process_authorize_nonce_account( &rpc_client, config, nonce_account, *nonce_authority, new_authority, ), // Create nonce account CliCommand::CreateNonceAccount { nonce_account, seed, nonce_authority, amount, } => process_create_nonce_account( &rpc_client, config, *nonce_account, seed.clone(), *nonce_authority, *amount, ), // Get the current nonce CliCommand::GetNonce(nonce_account_pubkey) => { process_get_nonce(&rpc_client, &nonce_account_pubkey) } // Get a new nonce CliCommand::NewNonce { nonce_account, nonce_authority, } => process_new_nonce(&rpc_client, config, nonce_account, *nonce_authority), // Show the contents of a nonce account CliCommand::ShowNonceAccount { nonce_account_pubkey, use_lamports_unit, } => process_show_nonce_account( &rpc_client, config, &nonce_account_pubkey, *use_lamports_unit, ), // Withdraw lamports from a nonce account CliCommand::WithdrawFromNonceAccount { nonce_account, nonce_authority, destination_account_pubkey, lamports, } => process_withdraw_from_nonce_account( &rpc_client, config, &nonce_account, *nonce_authority, &destination_account_pubkey, *lamports, ), // Program Deployment // Deploy a custom program to the chain CliCommand::Deploy(ref program_location) => { process_deploy(&rpc_client, config, program_location) } // Stake Commands // Create stake account CliCommand::CreateStakeAccount { stake_account, seed, staker, withdrawer, lockup, amount, sign_only, blockhash_query, ref nonce_account, nonce_authority, fee_payer, from, } => process_create_stake_account( &rpc_client, config, *stake_account, seed, staker, withdrawer, lockup, *amount, *sign_only, blockhash_query, nonce_account.as_ref(), *nonce_authority, *fee_payer, *from, ), CliCommand::DeactivateStake { stake_account_pubkey, stake_authority, sign_only, blockhash_query, nonce_account, nonce_authority, fee_payer, } => process_deactivate_stake_account( &rpc_client, config, &stake_account_pubkey, *stake_authority, *sign_only, blockhash_query, *nonce_account, *nonce_authority, *fee_payer, ), CliCommand::DelegateStake { stake_account_pubkey, vote_account_pubkey, stake_authority, force, sign_only, blockhash_query, nonce_account, nonce_authority, fee_payer, } => process_delegate_stake( &rpc_client, config, &stake_account_pubkey, &vote_account_pubkey, *stake_authority, *force, *sign_only, blockhash_query, *nonce_account, *nonce_authority, *fee_payer, ), CliCommand::SplitStake { stake_account_pubkey, stake_authority, sign_only, blockhash_query, nonce_account, nonce_authority, split_stake_account, seed, lamports, fee_payer, } => process_split_stake( &rpc_client, config, &stake_account_pubkey, *stake_authority, *sign_only, blockhash_query, *nonce_account, *nonce_authority, *split_stake_account, seed, *lamports, *fee_payer, ), CliCommand::ShowStakeAccount { pubkey: stake_account_pubkey, use_lamports_unit, } => process_show_stake_account( &rpc_client, config, &stake_account_pubkey, *use_lamports_unit, ), CliCommand::ShowStakeHistory { use_lamports_unit } => { process_show_stake_history(&rpc_client, config, *use_lamports_unit) } CliCommand::StakeAuthorize { stake_account_pubkey, ref new_authorizations, sign_only, blockhash_query, nonce_account, nonce_authority, fee_payer, } => process_stake_authorize( &rpc_client, config, &stake_account_pubkey, new_authorizations, *sign_only, blockhash_query, *nonce_account, *nonce_authority, *fee_payer, ), CliCommand::StakeSetLockup { stake_account_pubkey, mut lockup, custodian, sign_only, blockhash_query, nonce_account, nonce_authority, fee_payer, } => process_stake_set_lockup( &rpc_client, config, &stake_account_pubkey, &mut lockup, *custodian, *sign_only, blockhash_query, *nonce_account, *nonce_authority, *fee_payer, ), CliCommand::WithdrawStake { stake_account_pubkey, destination_account_pubkey, lamports, withdraw_authority, custodian, sign_only, blockhash_query, ref nonce_account, nonce_authority, fee_payer, } => process_withdraw_stake( &rpc_client, config, &stake_account_pubkey, &destination_account_pubkey, *lamports, *withdraw_authority, *custodian, *sign_only, blockhash_query, nonce_account.as_ref(), *nonce_authority, *fee_payer, ), // Validator Info Commands // Return all or single validator info CliCommand::GetValidatorInfo(info_pubkey) => { process_get_validator_info(&rpc_client, config, *info_pubkey) } // Publish validator info CliCommand::SetValidatorInfo { validator_info, force_keybase, info_pubkey, } => process_set_validator_info( &rpc_client, config, &validator_info, *force_keybase, *info_pubkey, ), // Vote Commands // Create vote account CliCommand::CreateVoteAccount { seed, identity_account, authorized_voter, authorized_withdrawer, commission, } => process_create_vote_account( &rpc_client, config, seed, *identity_account, authorized_voter, authorized_withdrawer, *commission, ), CliCommand::ShowVoteAccount { pubkey: vote_account_pubkey, use_lamports_unit, commitment_config, } => process_show_vote_account( &rpc_client, config, &vote_account_pubkey, *use_lamports_unit, *commitment_config, ), CliCommand::WithdrawFromVoteAccount { vote_account_pubkey, withdraw_authority, lamports, destination_account_pubkey, } => process_withdraw_from_vote_account( &rpc_client, config, vote_account_pubkey, *withdraw_authority, *lamports, destination_account_pubkey, ), CliCommand::VoteAuthorize { vote_account_pubkey, new_authorized_pubkey, vote_authorize, } => process_vote_authorize( &rpc_client, config, &vote_account_pubkey, &new_authorized_pubkey, *vote_authorize, ), CliCommand::VoteUpdateValidator { vote_account_pubkey, new_identity_account, } => process_vote_update_validator( &rpc_client, config, &vote_account_pubkey, *new_identity_account, ), // Wallet Commands // Request an airdrop from Solana Faucet; CliCommand::Airdrop { faucet_host, faucet_port, pubkey, lamports, } => { let faucet_addr = SocketAddr::new( faucet_host.unwrap_or_else(|| { let faucet_host = Url::parse(&config.json_rpc_url) .unwrap() .host() .unwrap() .to_string(); solana_net_utils::parse_host(&faucet_host).unwrap_or_else(|err| { panic!("Unable to resolve {}: {}", faucet_host, err); }) }), *faucet_port, ); process_airdrop(&rpc_client, config, &faucet_addr, pubkey, *lamports) } // Check client balance CliCommand::Balance { pubkey, use_lamports_unit, commitment_config, } => process_balance( &rpc_client, config, &pubkey, *use_lamports_unit, *commitment_config, ), // Cancel a contract by contract Pubkey CliCommand::Cancel(pubkey) => process_cancel(&rpc_client, config, &pubkey), // Confirm the last client transaction by signature CliCommand::Confirm(signature) => process_confirm(&rpc_client, config, signature), CliCommand::DecodeTransaction(transaction) => process_decode_transaction(transaction), // If client has positive balance, pay lamports to another address CliCommand::Pay(PayCommand { amount, to, timestamp, timestamp_pubkey, ref witnesses, cancelable, sign_only, blockhash_query, nonce_account, nonce_authority, }) => process_pay( &rpc_client, config, *amount, &to, *timestamp, *timestamp_pubkey, witnesses, *cancelable, *sign_only, blockhash_query, *nonce_account, *nonce_authority, ), CliCommand::ResolveSigner(path) => { if let Some(path) = path { Ok(path.to_string()) } else { Ok("Signer is valid".to_string()) } } CliCommand::ShowAccount { pubkey, output_file, use_lamports_unit, } => process_show_account( &rpc_client, config, &pubkey, &output_file, *use_lamports_unit, ), // Apply time elapsed to contract CliCommand::TimeElapsed(to, pubkey, dt) => { process_time_elapsed(&rpc_client, config, &to, &pubkey, *dt) } CliCommand::Transfer { amount, to, from, sign_only, no_wait, ref blockhash_query, ref nonce_account, nonce_authority, fee_payer, } => process_transfer( &rpc_client, config, *amount, to, *from, *sign_only, *no_wait, blockhash_query, nonce_account.as_ref(), *nonce_authority, *fee_payer, ), // Apply witness signature to contract CliCommand::Witness(to, pubkey) => process_witness(&rpc_client, config, &to, &pubkey), } } // Quick and dirty Keypair that assumes the client will do retries but not update the // blockhash. If the client updates the blockhash, the signature will be invalid. struct FaucetKeypair { transaction: Transaction, } impl FaucetKeypair { fn new_keypair( faucet_addr: &SocketAddr, to_pubkey: &Pubkey, lamports: u64, blockhash: Hash, ) -> Result> { let transaction = request_airdrop_transaction(faucet_addr, to_pubkey, lamports, blockhash)?; Ok(Self { transaction }) } fn airdrop_transaction(&self) -> Transaction { self.transaction.clone() } } impl Signer for FaucetKeypair { /// Return the public key of the keypair used to sign votes fn pubkey(&self) -> Pubkey { self.transaction.message().account_keys[0] } fn try_pubkey(&self) -> Result { Ok(self.pubkey()) } fn sign_message(&self, _msg: &[u8]) -> Signature { self.transaction.signatures[0] } fn try_sign_message(&self, message: &[u8]) -> Result { Ok(self.sign_message(message)) } } pub fn request_and_confirm_airdrop( rpc_client: &RpcClient, faucet_addr: &SocketAddr, to_pubkey: &Pubkey, lamports: u64, config: &CliConfig, ) -> ProcessResult { let (blockhash, _fee_calculator) = rpc_client.get_recent_blockhash()?; let keypair = { let mut retries = 5; loop { let result = FaucetKeypair::new_keypair(faucet_addr, to_pubkey, lamports, blockhash); if result.is_ok() || retries == 0 { break result; } retries -= 1; sleep(Duration::from_secs(1)); } }?; let tx = keypair.airdrop_transaction(); let result = rpc_client.send_and_confirm_transaction_with_spinner(&tx); log_instruction_custom_error::(result, &config) } pub fn log_instruction_custom_error( result: ClientResult, config: &CliConfig, ) -> ProcessResult where E: 'static + std::error::Error + DecodeError + FromPrimitive, { match result { Err(err) => { if let ClientErrorKind::TransactionError(TransactionError::InstructionError( _, InstructionError::Custom(code), )) = err.kind() { if let Some(specific_error) = E::decode_custom_error_to_enum(*code) { return Err(specific_error.into()); } } Err(err.into()) } Ok(sig) => { let signature = CliSignature { signature: sig.clone().to_string(), }; Ok(config.output_format.formatted_string(&signature)) } } } pub(crate) fn build_balance_message( lamports: u64, use_lamports_unit: bool, show_unit: bool, ) -> String { if use_lamports_unit { let ess = if lamports == 1 { "" } else { "s" }; let unit = if show_unit { format!(" lamport{}", ess) } else { "".to_string() }; format!("{:?}{}", lamports, unit) } else { let sol = lamports_to_sol(lamports); let sol_str = format!("{:.9}", sol); let pretty_sol = sol_str.trim_end_matches('0').trim_end_matches('.'); let unit = if show_unit { " SOL" } else { "" }; format!("{}{}", pretty_sol, unit) } } pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, 'v> { App::new(name) .about(about) .version(version) .setting(AppSettings::SubcommandRequiredElseHelp) .subcommand( SubCommand::with_name("address") .about("Get your public key") .arg( Arg::with_name("confirm_key") .long("confirm-key") .takes_value(false) .help("Confirm key on device; only relevant if using remote wallet"), ), ) .cluster_query_subcommands() .nonce_subcommands() .stake_subcommands() .subcommand( SubCommand::with_name("airdrop") .about("Request lamports") .arg( Arg::with_name("faucet_host") .long("faucet-host") .value_name("URL") .takes_value(true) .help("Faucet host to use [default: the --url host]"), ) .arg( Arg::with_name("faucet_port") .long("faucet-port") .value_name("PORT_NUMBER") .takes_value(true) .default_value(solana_faucet::faucet::FAUCET_PORT_STR) .help("Faucet port to use"), ) .arg( Arg::with_name("amount") .index(1) .value_name("AMOUNT") .takes_value(true) .validator(is_amount) .required(true) .help("The airdrop amount to request, in SOL"), ) .arg( pubkey!(Arg::with_name("to") .index(2) .value_name("RECIPIENT_ADDRESS"), "The account address of airdrop recipient. "), ), ) .subcommand( SubCommand::with_name("balance") .about("Get your balance") .arg( pubkey!(Arg::with_name("pubkey") .index(1) .value_name("ACCOUNT_ADDRESS"), "The account address of the balance to check. ") ) .arg( Arg::with_name("lamports") .long("lamports") .takes_value(false) .help("Display balance in lamports instead of SOL"), ) .arg(commitment_arg_with_default("max")), ) .subcommand( SubCommand::with_name("cancel") .about("Cancel a transfer") .arg( Arg::with_name("process_id") .index(1) .value_name("ACCOUNT_ADDRESS") .takes_value(true) .required(true) .validator(is_pubkey) .help("The account address of the transfer to cancel"), ), ) .subcommand( SubCommand::with_name("confirm") .about("Confirm transaction by signature") .arg( Arg::with_name("signature") .index(1) .value_name("TRANSACTION_SIGNATURE") .takes_value(true) .required(true) .help("The transaction signature to confirm"), ), ) .subcommand( SubCommand::with_name("decode-transaction") .about("Decode a base-58 binary transaction") .arg( Arg::with_name("base58_transaction") .index(1) .value_name("BASE58_TRANSACTION") .takes_value(true) .required(true) .help("The transaction to decode"), ), ) .subcommand( SubCommand::with_name("create-address-with-seed") .about("Generate a derived account address with a seed") .arg( Arg::with_name("seed") .index(1) .value_name("SEED_STRING") .takes_value(true) .required(true) .help("The seed. Must not take more than 32 bytes to encode as utf-8"), ) .arg( Arg::with_name("program_id") .index(2) .value_name("PROGRAM_ID") .takes_value(true) .required(true) .help( "The program_id that the address will ultimately be used for, \n\ or one of NONCE, STAKE, and VOTE keywords", ), ) .arg( pubkey!(Arg::with_name("from") .long("from") .value_name("FROM_PUBKEY") .required(false), "From (base) key, [default: cli config keypair]. "), ), ) .subcommand( SubCommand::with_name("deploy") .about("Deploy a program") .arg( Arg::with_name("program_location") .index(1) .value_name("PROGRAM_FILEPATH") .takes_value(true) .required(true) .help("/path/to/program.o"), ), ) .subcommand( SubCommand::with_name("pay") .about("Send a payment") .arg( pubkey!(Arg::with_name("to") .index(1) .value_name("RECIPIENT_ADDRESS") .required(true), "The account address of recipient. "), ) .arg( Arg::with_name("amount") .index(2) .value_name("AMOUNT") .takes_value(true) .validator(is_amount_or_all) .required(true) .help("The amount to send, in SOL; accepts keyword ALL"), ) .arg( Arg::with_name("timestamp") .long("after") .value_name("DATETIME") .takes_value(true) .help("A timestamp after which transaction will execute"), ) .arg( Arg::with_name("timestamp_pubkey") .long("require-timestamp-from") .value_name("PUBKEY") .takes_value(true) .requires("timestamp") .validator(is_pubkey) .help("Require timestamp from this third party"), ) .arg( Arg::with_name("witness") .long("require-signature-from") .value_name("PUBKEY") .takes_value(true) .multiple(true) .use_delimiter(true) .validator(is_pubkey) .help("Any third party signatures required to unlock the lamports"), ) .arg( Arg::with_name("cancelable") .long("cancelable") .takes_value(false), ) .offline_args() .arg(nonce_arg()) .arg(nonce_authority_arg()), ) .subcommand( SubCommand::with_name("resolve-signer") .about("Checks that a signer is valid, and returns its specific path; useful for signers that may be specified generally, eg. usb://ledger") .arg( Arg::with_name("signer") .index(1) .value_name("SIGNER_KEYPAIR") .takes_value(true) .required(true) .validator(is_valid_signer) .help("The signer path to resolve") ) ) .subcommand( SubCommand::with_name("send-signature") .about("Send a signature to authorize a transfer") .arg( pubkey!(Arg::with_name("to") .index(1) .value_name("RECIPIENT_ADDRESS") .required(true), "The account address of recipient. "), ) .arg( Arg::with_name("process_id") .index(2) .value_name("ACCOUNT_ADDRESS") .takes_value(true) .required(true) .help("The account address of the transfer to authorize"), ), ) .subcommand( SubCommand::with_name("send-timestamp") .about("Send a timestamp to unlock a transfer") .arg( pubkey!(Arg::with_name("to") .index(1) .value_name("RECIPIENT_ADDRESS") .required(true), "The account address of recipient. "), ) .arg( Arg::with_name("process_id") .index(2) .value_name("ACCOUNT_ADDRESS") .takes_value(true) .required(true) .help("The account address of the transfer to unlock"), ) .arg( Arg::with_name("datetime") .long("date") .value_name("DATETIME") .takes_value(true) .help("Optional arbitrary timestamp to apply"), ), ) .subcommand( SubCommand::with_name("transfer") .about("Transfer funds between system accounts") .arg( pubkey!(Arg::with_name("to") .index(1) .value_name("RECIPIENT_ADDRESS") .required(true), "The account address of recipient. "), ) .arg( Arg::with_name("amount") .index(2) .value_name("AMOUNT") .takes_value(true) .validator(is_amount_or_all) .required(true) .help("The amount to send, in SOL; accepts keyword ALL"), ) .arg( pubkey!(Arg::with_name("from") .long("from") .value_name("FROM_ADDRESS"), "Source account of funds (if different from client local account). "), ) .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"), ) .offline_args() .arg(nonce_arg()) .arg(nonce_authority_arg()) .arg(fee_payer_arg()), ) .subcommand( SubCommand::with_name("account") .about("Show the contents of an account") .alias("account") .arg( pubkey!(Arg::with_name("account_pubkey") .index(1) .value_name("ACCOUNT_ADDRESS") .required(true), "Account key URI. ") ) .arg( Arg::with_name("output_file") .long("output-file") .short("o") .value_name("FILEPATH") .takes_value(true) .help("Write the account data to this file"), ) .arg( Arg::with_name("lamports") .long("lamports") .takes_value(false) .help("Display balance in lamports instead of SOL"), ), ) .validator_info_subcommands() .vote_subcommands() } #[cfg(test)] mod tests { use super::*; use serde_json::Value; use solana_client::mock_rpc_client_request::SIGNATURE; use solana_sdk::{ pubkey::Pubkey, signature::{ keypair_from_seed, read_keypair_file, write_keypair_file, NullSigner, Presigner, }, transaction::TransactionError, }; use std::path::PathBuf; fn make_tmp_path(name: &str) -> String { let out_dir = std::env::var("FARF_DIR").unwrap_or_else(|_| "farf".to_string()); let keypair = Keypair::new(); let path = format!("{}/tmp/{}-{}", out_dir, name, keypair.pubkey()); // whack any possible collision let _ignored = std::fs::remove_dir_all(&path); // whack any possible collision let _ignored = std::fs::remove_file(&path); path } #[test] fn test_generate_unique_signers() { let matches = ArgMatches::default(); let default_keypair = Keypair::new(); let default_keypair_file = make_tmp_path("keypair_file"); write_keypair_file(&default_keypair, &default_keypair_file).unwrap(); let signer_info = generate_unique_signers(vec![], &matches, &default_keypair_file, &mut None).unwrap(); assert_eq!(signer_info.signers.len(), 0); let signer_info = generate_unique_signers(vec![None, None], &matches, &default_keypair_file, &mut None) .unwrap(); assert_eq!(signer_info.signers.len(), 1); assert_eq!(signer_info.index_of(None), Some(0)); assert_eq!(signer_info.index_of(Some(Pubkey::new_rand())), None); let keypair0 = keypair_from_seed(&[1u8; 32]).unwrap(); let keypair0_pubkey = keypair0.pubkey(); let keypair0_clone = keypair_from_seed(&[1u8; 32]).unwrap(); let keypair0_clone_pubkey = keypair0.pubkey(); let signers = vec![None, Some(keypair0.into()), Some(keypair0_clone.into())]; let signer_info = generate_unique_signers(signers, &matches, &default_keypair_file, &mut None).unwrap(); assert_eq!(signer_info.signers.len(), 2); assert_eq!(signer_info.index_of(None), Some(0)); assert_eq!(signer_info.index_of(Some(keypair0_pubkey)), Some(1)); assert_eq!(signer_info.index_of(Some(keypair0_clone_pubkey)), Some(1)); let keypair0 = keypair_from_seed(&[1u8; 32]).unwrap(); let keypair0_pubkey = keypair0.pubkey(); let keypair0_clone = keypair_from_seed(&[1u8; 32]).unwrap(); let signers = vec![Some(keypair0.into()), Some(keypair0_clone.into())]; let signer_info = generate_unique_signers(signers, &matches, &default_keypair_file, &mut None).unwrap(); assert_eq!(signer_info.signers.len(), 1); assert_eq!(signer_info.index_of(Some(keypair0_pubkey)), Some(0)); // Signers with the same pubkey are not distinct let keypair0 = keypair_from_seed(&[2u8; 32]).unwrap(); let keypair0_pubkey = keypair0.pubkey(); let keypair1 = keypair_from_seed(&[3u8; 32]).unwrap(); let keypair1_pubkey = keypair1.pubkey(); let message = vec![0, 1, 2, 3]; let presigner0 = Presigner::new(&keypair0.pubkey(), &keypair0.sign_message(&message)); let presigner0_pubkey = presigner0.pubkey(); let presigner1 = Presigner::new(&keypair1.pubkey(), &keypair1.sign_message(&message)); let presigner1_pubkey = presigner1.pubkey(); let signers = vec![ Some(keypair0.into()), Some(presigner0.into()), Some(presigner1.into()), Some(keypair1.into()), ]; let signer_info = generate_unique_signers(signers, &matches, &default_keypair_file, &mut None).unwrap(); assert_eq!(signer_info.signers.len(), 2); assert_eq!(signer_info.index_of(Some(keypair0_pubkey)), Some(0)); assert_eq!(signer_info.index_of(Some(keypair1_pubkey)), Some(1)); assert_eq!(signer_info.index_of(Some(presigner0_pubkey)), Some(0)); assert_eq!(signer_info.index_of(Some(presigner1_pubkey)), Some(1)); } #[test] fn test_cli_parse_command() { let test_commands = app("test", "desc", "version"); let pubkey = Pubkey::new_rand(); let pubkey_string = format!("{}", pubkey); let witness0 = Pubkey::new_rand(); let witness0_string = format!("{}", witness0); let witness1 = Pubkey::new_rand(); let witness1_string = format!("{}", witness1); let dt = Utc.ymd(2018, 9, 19).and_hms(17, 30, 59); // Test Airdrop Subcommand let test_airdrop = test_commands .clone() .get_matches_from(vec!["test", "airdrop", "50", &pubkey_string]); assert_eq!( parse_command(&test_airdrop, "", &mut None).unwrap(), CliCommandInfo { command: CliCommand::Airdrop { faucet_host: None, faucet_port: solana_faucet::faucet::FAUCET_PORT, pubkey: Some(pubkey), lamports: 50_000_000_000, }, signers: vec![], } ); // Test Balance Subcommand, incl pubkey and keypair-file inputs let default_keypair = Keypair::new(); let keypair_file = make_tmp_path("keypair_file"); write_keypair_file(&default_keypair, &keypair_file).unwrap(); let keypair = read_keypair_file(&keypair_file).unwrap(); let test_balance = test_commands.clone().get_matches_from(vec![ "test", "balance", &keypair.pubkey().to_string(), ]); assert_eq!( parse_command(&test_balance, "", &mut None).unwrap(), CliCommandInfo { command: CliCommand::Balance { pubkey: Some(keypair.pubkey()), use_lamports_unit: false, commitment_config: CommitmentConfig::default(), }, signers: vec![], } ); let test_balance = test_commands.clone().get_matches_from(vec![ "test", "balance", &keypair_file, "--lamports", ]); assert_eq!( parse_command(&test_balance, "", &mut None).unwrap(), CliCommandInfo { command: CliCommand::Balance { pubkey: Some(keypair.pubkey()), use_lamports_unit: true, commitment_config: CommitmentConfig::default(), }, signers: vec![], } ); let test_balance = test_commands .clone() .get_matches_from(vec!["test", "balance", "--lamports"]); assert_eq!( parse_command(&test_balance, &keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Balance { pubkey: None, use_lamports_unit: true, commitment_config: CommitmentConfig::default(), }, signers: vec![read_keypair_file(&keypair_file).unwrap().into()], } ); // Test Cancel Subcommand let test_cancel = test_commands .clone() .get_matches_from(vec!["test", "cancel", &pubkey_string]); assert_eq!( parse_command(&test_cancel, &keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Cancel(pubkey), signers: vec![read_keypair_file(&keypair_file).unwrap().into()], } ); // Test Confirm Subcommand let signature = Signature::new(&vec![1; 64]); let signature_string = format!("{:?}", signature); let test_confirm = test_commands .clone() .get_matches_from(vec!["test", "confirm", &signature_string]); assert_eq!( parse_command(&test_confirm, "", &mut None).unwrap(), CliCommandInfo { command: CliCommand::Confirm(signature), signers: vec![], } ); let test_bad_signature = test_commands .clone() .get_matches_from(vec!["test", "confirm", "deadbeef"]); assert!(parse_command(&test_bad_signature, "", &mut None).is_err()); // Test CreateAddressWithSeed let from_pubkey = Some(Pubkey::new_rand()); let from_str = from_pubkey.unwrap().to_string(); for (name, program_id) in &[ ("STAKE", solana_stake_program::id()), ("VOTE", solana_vote_program::id()), ("NONCE", system_program::id()), ] { let test_create_address_with_seed = test_commands.clone().get_matches_from(vec![ "test", "create-address-with-seed", "seed", name, "--from", &from_str, ]); assert_eq!( parse_command(&test_create_address_with_seed, "", &mut None).unwrap(), CliCommandInfo { command: CliCommand::CreateAddressWithSeed { from_pubkey, seed: "seed".to_string(), program_id: *program_id }, signers: vec![], } ); } let test_create_address_with_seed = test_commands.clone().get_matches_from(vec![ "test", "create-address-with-seed", "seed", "STAKE", ]); assert_eq!( parse_command(&test_create_address_with_seed, &keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::CreateAddressWithSeed { from_pubkey: None, seed: "seed".to_string(), program_id: solana_stake_program::id(), }, signers: vec![read_keypair_file(&keypair_file).unwrap().into()], } ); // Test Deploy Subcommand let test_deploy = test_commands .clone() .get_matches_from(vec!["test", "deploy", "/Users/test/program.o"]); assert_eq!( parse_command(&test_deploy, &keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Deploy("/Users/test/program.o".to_string()), signers: vec![read_keypair_file(&keypair_file).unwrap().into()], } ); // Test ResolveSigner Subcommand, KeypairUrl::Filepath let test_resolve_signer = test_commands .clone() .get_matches_from(vec!["test", "resolve-signer", &keypair_file]); assert_eq!( parse_command(&test_resolve_signer, "", &mut None).unwrap(), CliCommandInfo { command: CliCommand::ResolveSigner(Some(keypair_file.clone())), signers: vec![], } ); // Test ResolveSigner Subcommand, KeypairUrl::Pubkey (Presigner) let test_resolve_signer = test_commands .clone() .get_matches_from(vec!["test", "resolve-signer", &pubkey_string]); assert_eq!( parse_command(&test_resolve_signer, "", &mut None).unwrap(), CliCommandInfo { command: CliCommand::ResolveSigner(Some(pubkey.to_string())), signers: vec![], } ); // Test Simple Pay Subcommand let test_pay = test_commands .clone() .get_matches_from(vec!["test", "pay", &pubkey_string, "50"]); assert_eq!( parse_command(&test_pay, &keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Pay(PayCommand { amount: SpendAmount::Some(50_000_000_000), to: pubkey, ..PayCommand::default() }), signers: vec![read_keypair_file(&keypair_file).unwrap().into()], } ); // Test Pay Subcommand w/ Witness let test_pay_multiple_witnesses = test_commands.clone().get_matches_from(vec![ "test", "pay", &pubkey_string, "50", "--require-signature-from", &witness0_string, "--require-signature-from", &witness1_string, ]); assert_eq!( parse_command(&test_pay_multiple_witnesses, &keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Pay(PayCommand { amount: SpendAmount::Some(50_000_000_000), to: pubkey, witnesses: Some(vec![witness0, witness1]), ..PayCommand::default() }), signers: vec![read_keypair_file(&keypair_file).unwrap().into()], } ); let test_pay_single_witness = test_commands.clone().get_matches_from(vec![ "test", "pay", &pubkey_string, "50", "--require-signature-from", &witness0_string, ]); assert_eq!( parse_command(&test_pay_single_witness, &keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Pay(PayCommand { amount: SpendAmount::Some(50_000_000_000), to: pubkey, witnesses: Some(vec![witness0]), ..PayCommand::default() }), signers: vec![read_keypair_file(&keypair_file).unwrap().into()], } ); // Test Pay Subcommand w/ Timestamp let test_pay_timestamp = test_commands.clone().get_matches_from(vec![ "test", "pay", &pubkey_string, "50", "--after", "2018-09-19T17:30:59", "--require-timestamp-from", &witness0_string, ]); assert_eq!( parse_command(&test_pay_timestamp, &keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Pay(PayCommand { amount: SpendAmount::Some(50_000_000_000), to: pubkey, timestamp: Some(dt), timestamp_pubkey: Some(witness0), ..PayCommand::default() }), signers: vec![read_keypair_file(&keypair_file).unwrap().into()], } ); // Test Pay Subcommand w/ sign-only let blockhash = Hash::default(); let blockhash_string = format!("{}", blockhash); let test_pay = test_commands.clone().get_matches_from(vec![ "test", "pay", &pubkey_string, "50", "--blockhash", &blockhash_string, "--sign-only", ]); assert_eq!( parse_command(&test_pay, &keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Pay(PayCommand { amount: SpendAmount::Some(50_000_000_000), to: pubkey, blockhash_query: BlockhashQuery::None(blockhash), sign_only: true, ..PayCommand::default() }), signers: vec![read_keypair_file(&keypair_file).unwrap().into()], } ); // Test Pay Subcommand w/ Blockhash let test_pay = test_commands.clone().get_matches_from(vec![ "test", "pay", &pubkey_string, "50", "--blockhash", &blockhash_string, ]); assert_eq!( parse_command(&test_pay, &keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Pay(PayCommand { amount: SpendAmount::Some(50_000_000_000), to: pubkey, blockhash_query: BlockhashQuery::FeeCalculator( blockhash_query::Source::Cluster, blockhash ), ..PayCommand::default() }), signers: vec![read_keypair_file(&keypair_file).unwrap().into()], } ); // Test Pay Subcommand w/ Nonce let blockhash = Hash::default(); let blockhash_string = format!("{}", blockhash); let test_pay = test_commands.clone().get_matches_from(vec![ "test", "pay", &pubkey_string, "50", "--blockhash", &blockhash_string, "--nonce", &pubkey_string, ]); assert_eq!( parse_command(&test_pay, &keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Pay(PayCommand { amount: SpendAmount::Some(50_000_000_000), to: pubkey, blockhash_query: BlockhashQuery::FeeCalculator( blockhash_query::Source::NonceAccount(pubkey), blockhash ), nonce_account: Some(pubkey), ..PayCommand::default() }), signers: vec![read_keypair_file(&keypair_file).unwrap().into()], } ); // Test Pay Subcommand w/ Nonce and Nonce Authority let blockhash = Hash::default(); let blockhash_string = format!("{}", blockhash); let keypair = read_keypair_file(&keypair_file).unwrap(); let test_pay = test_commands.clone().get_matches_from(vec![ "test", "pay", &pubkey_string, "50", "--blockhash", &blockhash_string, "--nonce", &pubkey_string, "--nonce-authority", &keypair_file, ]); assert_eq!( parse_command(&test_pay, &keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Pay(PayCommand { amount: SpendAmount::Some(50_000_000_000), to: pubkey, blockhash_query: BlockhashQuery::FeeCalculator( blockhash_query::Source::NonceAccount(pubkey), blockhash ), nonce_account: Some(pubkey), nonce_authority: 0, ..PayCommand::default() }), signers: vec![keypair.into()], } ); // Test Pay Subcommand w/ Nonce and Offline Nonce Authority let keypair = read_keypair_file(&keypair_file).unwrap(); let authority_pubkey = keypair.pubkey(); let authority_pubkey_string = format!("{}", authority_pubkey); let sig = keypair.sign_message(&[0u8]); let signer_arg = format!("{}={}", authority_pubkey, sig); let test_pay = test_commands.clone().get_matches_from(vec![ "test", "pay", &pubkey_string, "50", "--blockhash", &blockhash_string, "--nonce", &pubkey_string, "--nonce-authority", &authority_pubkey_string, "--signer", &signer_arg, ]); assert_eq!( parse_command(&test_pay, &keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Pay(PayCommand { amount: SpendAmount::Some(50_000_000_000), to: pubkey, blockhash_query: BlockhashQuery::FeeCalculator( blockhash_query::Source::NonceAccount(pubkey), blockhash ), nonce_account: Some(pubkey), nonce_authority: 0, ..PayCommand::default() }), signers: vec![keypair.into()], } ); // Test Pay Subcommand w/ Nonce and Offline Nonce Authority // authority pubkey not in signers fails let keypair = read_keypair_file(&keypair_file).unwrap(); let authority_pubkey = keypair.pubkey(); let authority_pubkey_string = format!("{}", authority_pubkey); let sig = keypair.sign_message(&[0u8]); let signer_arg = format!("{}={}", Pubkey::new_rand(), sig); let test_pay = test_commands.clone().get_matches_from(vec![ "test", "pay", &pubkey_string, "50", "--blockhash", &blockhash_string, "--nonce", &pubkey_string, "--nonce-authority", &authority_pubkey_string, "--signer", &signer_arg, ]); assert!(parse_command(&test_pay, &keypair_file, &mut None).is_err()); // Test Send-Signature Subcommand let test_send_signature = test_commands.clone().get_matches_from(vec![ "test", "send-signature", &pubkey_string, &pubkey_string, ]); assert_eq!( parse_command(&test_send_signature, &keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Witness(pubkey, pubkey), signers: vec![read_keypair_file(&keypair_file).unwrap().into()], } ); let test_pay_multiple_witnesses = test_commands.clone().get_matches_from(vec![ "test", "pay", &pubkey_string, "50", "--after", "2018-09-19T17:30:59", "--require-signature-from", &witness0_string, "--require-timestamp-from", &witness0_string, "--require-signature-from", &witness1_string, ]); assert_eq!( parse_command(&test_pay_multiple_witnesses, &keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Pay(PayCommand { amount: SpendAmount::Some(50_000_000_000), to: pubkey, timestamp: Some(dt), timestamp_pubkey: Some(witness0), witnesses: Some(vec![witness0, witness1]), ..PayCommand::default() }), signers: vec![read_keypair_file(&keypair_file).unwrap().into()], } ); // Test Send-Timestamp Subcommand let test_send_timestamp = test_commands.clone().get_matches_from(vec![ "test", "send-timestamp", &pubkey_string, &pubkey_string, "--date", "2018-09-19T17:30:59", ]); assert_eq!( parse_command(&test_send_timestamp, &keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::TimeElapsed(pubkey, pubkey, dt), signers: vec![read_keypair_file(&keypair_file).unwrap().into()], } ); let test_bad_timestamp = test_commands.clone().get_matches_from(vec![ "test", "send-timestamp", &pubkey_string, &pubkey_string, "--date", "20180919T17:30:59", ]); assert!(parse_command(&test_bad_timestamp, &keypair_file, &mut None).is_err()); } #[test] fn test_cli_process_command() { // Success cases let mut config = CliConfig::default(); config.rpc_client = Some(RpcClient::new_mock("succeeds".to_string())); let keypair = Keypair::new(); let pubkey = keypair.pubkey().to_string(); config.signers = vec![&keypair]; config.command = CliCommand::Address; assert_eq!(process_command(&config).unwrap(), pubkey); config.command = CliCommand::Balance { pubkey: None, use_lamports_unit: true, commitment_config: CommitmentConfig::default(), }; assert_eq!(process_command(&config).unwrap(), "50 lamports"); config.command = CliCommand::Balance { pubkey: None, use_lamports_unit: false, commitment_config: CommitmentConfig::default(), }; assert_eq!(process_command(&config).unwrap(), "0.00000005 SOL"); let process_id = Pubkey::new_rand(); config.command = CliCommand::Cancel(process_id); assert!(process_command(&config).is_ok()); let good_signature = Signature::new(&bs58::decode(SIGNATURE).into_vec().unwrap()); config.command = CliCommand::Confirm(good_signature); assert_eq!(process_command(&config).unwrap(), "Confirmed"); let bob_keypair = Keypair::new(); let bob_pubkey = bob_keypair.pubkey(); let identity_keypair = Keypair::new(); config.command = CliCommand::CreateVoteAccount { seed: None, identity_account: 2, authorized_voter: Some(bob_pubkey), authorized_withdrawer: Some(bob_pubkey), commission: 0, }; config.signers = vec![&keypair, &bob_keypair, &identity_keypair]; let result = process_command(&config); assert!(result.is_ok()); let new_authorized_pubkey = Pubkey::new_rand(); config.signers = vec![&bob_keypair]; config.command = CliCommand::VoteAuthorize { vote_account_pubkey: bob_pubkey, new_authorized_pubkey, vote_authorize: VoteAuthorize::Voter, }; let result = process_command(&config); assert!(result.is_ok()); let new_identity_keypair = Keypair::new(); config.signers = vec![&keypair, &bob_keypair, &new_identity_keypair]; config.command = CliCommand::VoteUpdateValidator { vote_account_pubkey: bob_pubkey, new_identity_account: 2, }; let result = process_command(&config); assert!(result.is_ok()); let bob_keypair = Keypair::new(); let bob_pubkey = bob_keypair.pubkey(); let custodian = Pubkey::new_rand(); config.command = CliCommand::CreateStakeAccount { stake_account: 1, seed: None, staker: None, withdrawer: None, lockup: Lockup { epoch: 0, unix_timestamp: 0, custodian, }, amount: SpendAmount::Some(30), sign_only: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), nonce_account: None, nonce_authority: 0, fee_payer: 0, from: 0, }; config.signers = vec![&keypair, &bob_keypair]; let result = process_command(&config); assert!(result.is_ok()); let stake_pubkey = Pubkey::new_rand(); let to_pubkey = Pubkey::new_rand(); config.command = CliCommand::WithdrawStake { stake_account_pubkey: stake_pubkey, destination_account_pubkey: to_pubkey, lamports: 100, withdraw_authority: 0, custodian: None, sign_only: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), nonce_account: None, nonce_authority: 0, fee_payer: 0, }; config.signers = vec![&keypair]; let result = process_command(&config); assert!(result.is_ok()); let stake_pubkey = Pubkey::new_rand(); config.command = CliCommand::DeactivateStake { stake_account_pubkey: stake_pubkey, stake_authority: 0, sign_only: false, blockhash_query: BlockhashQuery::default(), nonce_account: None, nonce_authority: 0, fee_payer: 0, }; let result = process_command(&config); assert!(result.is_ok()); let stake_pubkey = Pubkey::new_rand(); let split_stake_account = Keypair::new(); config.command = CliCommand::SplitStake { stake_account_pubkey: stake_pubkey, stake_authority: 0, sign_only: false, blockhash_query: BlockhashQuery::default(), nonce_account: None, nonce_authority: 0, split_stake_account: 1, seed: None, lamports: 30, fee_payer: 0, }; config.signers = vec![&keypair, &split_stake_account]; let result = process_command(&config); assert!(result.is_ok()); config.command = CliCommand::GetSlot { commitment_config: CommitmentConfig::default(), }; assert_eq!(process_command(&config).unwrap(), "0"); config.command = CliCommand::GetTransactionCount { commitment_config: CommitmentConfig::default(), }; assert_eq!(process_command(&config).unwrap(), "1234"); config.signers = vec![&keypair]; config.command = CliCommand::Pay(PayCommand { amount: SpendAmount::Some(10), to: bob_pubkey, ..PayCommand::default() }); let result = process_command(&config); assert!(result.is_ok()); let date_string = "\"2018-09-19T17:30:59Z\""; let dt: DateTime = serde_json::from_str(&date_string).unwrap(); config.command = CliCommand::Pay(PayCommand { amount: SpendAmount::Some(10), to: bob_pubkey, timestamp: Some(dt), timestamp_pubkey: Some(config.signers[0].pubkey()), ..PayCommand::default() }); let result = process_command(&config); assert!(result.is_ok()); let witness = Pubkey::new_rand(); config.command = CliCommand::Pay(PayCommand { amount: SpendAmount::Some(10), to: bob_pubkey, witnesses: Some(vec![witness]), cancelable: true, ..PayCommand::default() }); let result = process_command(&config); assert!(result.is_ok()); let process_id = Pubkey::new_rand(); config.command = CliCommand::TimeElapsed(bob_pubkey, process_id, dt); config.signers = vec![&keypair]; let result = process_command(&config); assert!(result.is_ok()); let witness = Pubkey::new_rand(); config.command = CliCommand::Witness(bob_pubkey, witness); let result = process_command(&config); assert!(result.is_ok()); // CreateAddressWithSeed let from_pubkey = Pubkey::new_rand(); config.signers = vec![]; config.command = CliCommand::CreateAddressWithSeed { from_pubkey: Some(from_pubkey), seed: "seed".to_string(), program_id: solana_stake_program::id(), }; let address = process_command(&config); let expected_address = Pubkey::create_with_seed(&from_pubkey, "seed", &solana_stake_program::id()).unwrap(); assert_eq!(address.unwrap(), expected_address.to_string()); // Need airdrop cases let to = Pubkey::new_rand(); config.signers = vec![&keypair]; config.command = CliCommand::Airdrop { faucet_host: None, faucet_port: 1234, pubkey: Some(to), lamports: 50, }; assert!(process_command(&config).is_ok()); config.command = CliCommand::TimeElapsed(bob_pubkey, process_id, dt); let result = process_command(&config); assert!(result.is_ok()); let witness = Pubkey::new_rand(); config.command = CliCommand::Witness(bob_pubkey, witness); let result = process_command(&config); assert!(result.is_ok()); // sig_not_found case config.rpc_client = Some(RpcClient::new_mock("sig_not_found".to_string())); let missing_signature = Signature::new(&bs58::decode("5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW").into_vec().unwrap()); config.command = CliCommand::Confirm(missing_signature); assert_eq!(process_command(&config).unwrap(), "Not found"); // Tx error case config.rpc_client = Some(RpcClient::new_mock("account_in_use".to_string())); let any_signature = Signature::new(&bs58::decode(SIGNATURE).into_vec().unwrap()); config.command = CliCommand::Confirm(any_signature); assert_eq!( process_command(&config).unwrap(), format!("Transaction failed: {}", TransactionError::AccountInUse) ); // Failure cases config.rpc_client = Some(RpcClient::new_mock("fails".to_string())); config.command = CliCommand::Airdrop { faucet_host: None, faucet_port: 1234, pubkey: None, lamports: 50, }; assert!(process_command(&config).is_err()); config.command = CliCommand::Balance { pubkey: None, use_lamports_unit: false, commitment_config: CommitmentConfig::default(), }; assert!(process_command(&config).is_err()); let bob_keypair = Keypair::new(); let identity_keypair = Keypair::new(); config.command = CliCommand::CreateVoteAccount { seed: None, identity_account: 2, authorized_voter: Some(bob_pubkey), authorized_withdrawer: Some(bob_pubkey), commission: 0, }; config.signers = vec![&keypair, &bob_keypair, &identity_keypair]; assert!(process_command(&config).is_err()); config.command = CliCommand::VoteAuthorize { vote_account_pubkey: bob_pubkey, new_authorized_pubkey: bob_pubkey, vote_authorize: VoteAuthorize::Voter, }; assert!(process_command(&config).is_err()); config.command = CliCommand::VoteUpdateValidator { vote_account_pubkey: bob_pubkey, new_identity_account: 1, }; assert!(process_command(&config).is_err()); config.command = CliCommand::GetSlot { commitment_config: CommitmentConfig::default(), }; assert!(process_command(&config).is_err()); config.command = CliCommand::GetTransactionCount { commitment_config: CommitmentConfig::default(), }; assert!(process_command(&config).is_err()); config.command = CliCommand::Pay(PayCommand { amount: SpendAmount::Some(10), to: bob_pubkey, ..PayCommand::default() }); assert!(process_command(&config).is_err()); config.command = CliCommand::Pay(PayCommand { amount: SpendAmount::Some(10), to: bob_pubkey, timestamp: Some(dt), timestamp_pubkey: Some(config.signers[0].pubkey()), ..PayCommand::default() }); assert!(process_command(&config).is_err()); config.command = CliCommand::Pay(PayCommand { amount: SpendAmount::Some(10), to: bob_pubkey, witnesses: Some(vec![witness]), cancelable: true, ..PayCommand::default() }); assert!(process_command(&config).is_err()); config.command = CliCommand::TimeElapsed(bob_pubkey, process_id, dt); assert!(process_command(&config).is_err()); } #[test] fn test_cli_deploy() { solana_logger::setup(); let mut pathbuf = PathBuf::from(env!("CARGO_MANIFEST_DIR")); pathbuf.push("tests"); pathbuf.push("fixtures"); pathbuf.push("noop"); pathbuf.set_extension("so"); // Success case let mut config = CliConfig::default(); config.rpc_client = Some(RpcClient::new_mock("deploy_succeeds".to_string())); let default_keypair = Keypair::new(); config.signers = vec![&default_keypair]; config.command = CliCommand::Deploy(pathbuf.to_str().unwrap().to_string()); let result = process_command(&config); let json: Value = serde_json::from_str(&result.unwrap()).unwrap(); let program_id = json .as_object() .unwrap() .get("programId") .unwrap() .as_str() .unwrap(); assert!(program_id.parse::().is_ok()); // Failure case config.command = CliCommand::Deploy("bad/file/location.so".to_string()); assert!(process_command(&config).is_err()); } #[test] fn test_parse_transfer_subcommand() { let test_commands = app("test", "desc", "version"); let default_keypair = Keypair::new(); let default_keypair_file = make_tmp_path("keypair_file"); write_keypair_file(&default_keypair, &default_keypair_file).unwrap(); //Test Transfer Subcommand, SOL let from_keypair = keypair_from_seed(&[0u8; 32]).unwrap(); let from_pubkey = from_keypair.pubkey(); let from_string = from_pubkey.to_string(); let to_keypair = keypair_from_seed(&[1u8; 32]).unwrap(); let to_pubkey = to_keypair.pubkey(); let to_string = to_pubkey.to_string(); let test_transfer = test_commands .clone() .get_matches_from(vec!["test", "transfer", &to_string, "42"]); assert_eq!( parse_command(&test_transfer, &default_keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Transfer { amount: SpendAmount::Some(42_000_000_000), to: to_pubkey, from: 0, sign_only: false, no_wait: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), nonce_account: None, nonce_authority: 0, fee_payer: 0, }, signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()], } ); // Test Transfer ALL let test_transfer = test_commands .clone() .get_matches_from(vec!["test", "transfer", &to_string, "ALL"]); assert_eq!( parse_command(&test_transfer, &default_keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Transfer { amount: SpendAmount::All, to: to_pubkey, from: 0, sign_only: false, no_wait: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), nonce_account: None, nonce_authority: 0, fee_payer: 0, }, signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()], } ); // Test Transfer no-wait let test_transfer = test_commands.clone().get_matches_from(vec![ "test", "transfer", "--no-wait", &to_string, "42", ]); assert_eq!( parse_command(&test_transfer, &default_keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Transfer { amount: SpendAmount::Some(42_000_000_000), to: to_pubkey, from: 0, sign_only: false, no_wait: true, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), nonce_account: None, nonce_authority: 0, fee_payer: 0, }, signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()], } ); //Test Transfer Subcommand, offline sign let blockhash = Hash::new(&[1u8; 32]); let blockhash_string = blockhash.to_string(); let test_transfer = test_commands.clone().get_matches_from(vec![ "test", "transfer", &to_string, "42", "--blockhash", &blockhash_string, "--sign-only", ]); assert_eq!( parse_command(&test_transfer, &default_keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Transfer { amount: SpendAmount::Some(42_000_000_000), to: to_pubkey, from: 0, sign_only: true, no_wait: false, blockhash_query: BlockhashQuery::None(blockhash), nonce_account: None, nonce_authority: 0, fee_payer: 0, }, signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()], } ); //Test Transfer Subcommand, submit offline `from` let from_sig = from_keypair.sign_message(&[0u8]); let from_signer = format!("{}={}", from_pubkey, from_sig); let test_transfer = test_commands.clone().get_matches_from(vec![ "test", "transfer", &to_string, "42", "--from", &from_string, "--fee-payer", &from_string, "--signer", &from_signer, "--blockhash", &blockhash_string, ]); assert_eq!( parse_command(&test_transfer, &default_keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Transfer { amount: SpendAmount::Some(42_000_000_000), to: to_pubkey, from: 0, sign_only: false, no_wait: false, blockhash_query: BlockhashQuery::FeeCalculator( blockhash_query::Source::Cluster, blockhash ), nonce_account: None, nonce_authority: 0, fee_payer: 0, }, signers: vec![Presigner::new(&from_pubkey, &from_sig).into()], } ); //Test Transfer Subcommand, with nonce let nonce_address = Pubkey::new(&[1u8; 32]); let nonce_address_string = nonce_address.to_string(); let nonce_authority = keypair_from_seed(&[2u8; 32]).unwrap(); let nonce_authority_file = make_tmp_path("nonce_authority_file"); write_keypair_file(&nonce_authority, &nonce_authority_file).unwrap(); let test_transfer = test_commands.clone().get_matches_from(vec![ "test", "transfer", &to_string, "42", "--blockhash", &blockhash_string, "--nonce", &nonce_address_string, "--nonce-authority", &nonce_authority_file, ]); assert_eq!( parse_command(&test_transfer, &default_keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Transfer { amount: SpendAmount::Some(42_000_000_000), to: to_pubkey, from: 0, sign_only: false, no_wait: false, blockhash_query: BlockhashQuery::FeeCalculator( blockhash_query::Source::NonceAccount(nonce_address), blockhash ), nonce_account: Some(nonce_address.into()), nonce_authority: 1, fee_payer: 0, }, signers: vec![ read_keypair_file(&default_keypair_file).unwrap().into(), read_keypair_file(&nonce_authority_file).unwrap().into() ], } ); } #[test] fn test_return_signers() { struct BadSigner { pubkey: Pubkey, } impl BadSigner { pub fn new(pubkey: Pubkey) -> Self { Self { pubkey } } } impl Signer for BadSigner { fn try_pubkey(&self) -> Result { Ok(self.pubkey) } fn try_sign_message(&self, _message: &[u8]) -> Result { Ok(Signature::new(&[1u8; 64])) } } let mut config = CliConfig::default(); config.output_format = OutputFormat::JsonCompact; let present: Box = Box::new(keypair_from_seed(&[2u8; 32]).unwrap()); let absent: Box = Box::new(NullSigner::new(&Pubkey::new(&[3u8; 32]))); let bad: Box = Box::new(BadSigner::new(Pubkey::new(&[4u8; 32]))); let to = Pubkey::new(&[5u8; 32]); let nonce = Pubkey::new(&[6u8; 32]); let from = present.pubkey(); let fee_payer = absent.pubkey(); let nonce_auth = bad.pubkey(); let mut tx = Transaction::new_unsigned(Message::new_with_nonce( vec![system_instruction::transfer(&from, &to, 42)], Some(&fee_payer), &nonce, &nonce_auth, )); let signers = vec![present.as_ref(), absent.as_ref(), bad.as_ref()]; let blockhash = Hash::new(&[7u8; 32]); tx.try_partial_sign(&signers, blockhash).unwrap(); let res = return_signers(&tx, &config).unwrap(); let sign_only = parse_sign_only_reply_string(&res); assert_eq!(sign_only.blockhash, blockhash); assert_eq!(sign_only.present_signers[0].0, present.pubkey()); assert_eq!(sign_only.absent_signers[0], absent.pubkey()); assert_eq!(sign_only.bad_signers[0], bad.pubkey()); } }