solana/cli/src/cli.rs

3839 lines
132 KiB
Rust

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<Box<dyn Signer>>;
pub type SignerIndex = usize;
pub(crate) struct CliSignerInfo {
pub signers: CliSigners,
}
impl CliSignerInfo {
pub(crate) fn index_of(&self, pubkey: Option<Pubkey>) -> Option<usize> {
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<Option<Box<dyn Signer>>>,
matches: &ArgMatches<'_>,
default_signer_path: &str,
wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
) -> Result<CliSignerInfo, Box<dyn error::Error>> {
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<Keypair> 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<DateTime<Utc>>,
pub timestamp_pubkey: Option<Pubkey>,
pub witnesses: Option<Vec<Pubkey>>,
pub cancelable: bool,
pub sign_only: bool,
pub blockhash_query: BlockhashQuery,
pub nonce_account: Option<Pubkey>,
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<String>,
commitment_config: CommitmentConfig,
follow: bool,
},
ClusterDate,
ClusterVersion,
CreateAddressWithSeed {
from_pubkey: Option<Pubkey>,
seed: String,
program_id: Pubkey,
},
Fees,
GetBlockTime {
slot: Option<Slot>,
},
GetEpochInfo {
commitment_config: CommitmentConfig,
},
GetGenesisHash,
GetEpoch {
commitment_config: CommitmentConfig,
},
GetSlot {
commitment_config: CommitmentConfig,
},
LargestAccounts {
commitment_config: CommitmentConfig,
filter: Option<RpcLargestAccountsFilter>,
},
Supply {
commitment_config: CommitmentConfig,
print_accounts: bool,
},
TotalSupply {
commitment_config: CommitmentConfig,
},
GetTransactionCount {
commitment_config: CommitmentConfig,
},
LeaderSchedule,
LiveSlots,
Ping {
lamports: u64,
interval: Duration,
count: Option<u64>,
timeout: Duration,
commitment_config: CommitmentConfig,
},
ShowBlockProduction {
epoch: Option<Epoch>,
slot_limit: Option<u64>,
},
ShowGossip,
ShowStakes {
use_lamports_unit: bool,
vote_account_pubkeys: Option<Vec<Pubkey>>,
},
ShowValidators {
use_lamports_unit: bool,
commitment_config: CommitmentConfig,
},
TransactionHistory {
address: Pubkey,
end_slot: Option<Slot>, // None == latest slot
slot_limit: u64,
},
// Nonce commands
AuthorizeNonceAccount {
nonce_account: Pubkey,
nonce_authority: SignerIndex,
new_authority: Pubkey,
},
CreateNonceAccount {
nonce_account: SignerIndex,
seed: Option<String>,
nonce_authority: Option<Pubkey>,
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<String>,
staker: Option<Pubkey>,
withdrawer: Option<Pubkey>,
lockup: Lockup,
amount: SpendAmount,
sign_only: bool,
blockhash_query: BlockhashQuery,
nonce_account: Option<Pubkey>,
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<Pubkey>,
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<Pubkey>,
nonce_authority: SignerIndex,
fee_payer: SignerIndex,
},
SplitStake {
stake_account_pubkey: Pubkey,
stake_authority: SignerIndex,
sign_only: bool,
blockhash_query: BlockhashQuery,
nonce_account: Option<Pubkey>,
nonce_authority: SignerIndex,
split_stake_account: SignerIndex,
seed: Option<String>,
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<Pubkey>,
nonce_authority: SignerIndex,
fee_payer: SignerIndex,
},
StakeSetLockup {
stake_account_pubkey: Pubkey,
lockup: LockupArgs,
custodian: SignerIndex,
sign_only: bool,
blockhash_query: BlockhashQuery,
nonce_account: Option<Pubkey>,
nonce_authority: SignerIndex,
fee_payer: SignerIndex,
},
WithdrawStake {
stake_account_pubkey: Pubkey,
destination_account_pubkey: Pubkey,
lamports: u64,
withdraw_authority: SignerIndex,
custodian: Option<SignerIndex>,
sign_only: bool,
blockhash_query: BlockhashQuery,
nonce_account: Option<Pubkey>,
nonce_authority: SignerIndex,
fee_payer: SignerIndex,
},
// Validator Info Commands
GetValidatorInfo(Option<Pubkey>),
SetValidatorInfo {
validator_info: Value,
force_keybase: bool,
info_pubkey: Option<Pubkey>,
},
// Vote Commands
CreateVoteAccount {
seed: Option<String>,
identity_account: SignerIndex,
authorized_voter: Option<Pubkey>,
authorized_withdrawer: Option<Pubkey>,
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<IpAddr>,
faucet_port: u16,
pubkey: Option<Pubkey>,
lamports: u64,
},
Balance {
pubkey: Option<Pubkey>,
use_lamports_unit: bool,
commitment_config: CommitmentConfig,
},
Cancel(Pubkey),
Confirm(Signature),
DecodeTransaction(Transaction),
Pay(PayCommand),
ResolveSigner(Option<String>),
ShowAccount {
pubkey: Pubkey,
output_file: Option<String>,
use_lamports_unit: bool,
},
TimeElapsed(Pubkey, Pubkey, DateTime<Utc>), // TimeElapsed(to, process_id, timestamp)
Transfer {
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,
},
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<Box<dyn error::Error>> for CliError {
fn from(error: Box<dyn error::Error>) -> Self {
CliError::DynamicProgramError(error.to_string())
}
}
impl From<CliNonceError> 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<RpcClient>,
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<Pubkey, SignerError> {
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<Arc<RemoteWalletManager>>,
) -> Result<CliCommandInfo, Box<dyn error::Error>> {
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<String, Box<dyn std::error::Error>>;
pub fn get_blockhash_and_fee_calculator(
rpc_client: &RpcClient,
sign_only: bool,
blockhash: Option<Hash>,
) -> Result<(Hash, FeeCalculator), Box<dyn std::error::Error>> {
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<Arc<RemoteWalletManager>>,
) -> Result<CliCommandInfo, CliError> {
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<Pubkey>,
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<Pubkey>,
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<String>,
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::<SystemError>(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<DateTime<Utc>>,
timestamp_pubkey: Option<Pubkey>,
witnesses: &Option<Vec<Pubkey>>,
cancelable: bool,
sign_only: bool,
blockhash_query: &BlockhashQuery,
nonce_account: Option<Pubkey>,
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::<SystemError>(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::<BudgetError>(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::<BudgetError>(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::<BudgetError>(result, &config)
}
fn process_time_elapsed(
rpc_client: &RpcClient,
config: &CliConfig,
to: &Pubkey,
pubkey: &Pubkey,
dt: DateTime<Utc>,
) -> 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::<BudgetError>(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::<SystemError>(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::<BudgetError>(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<Self, Box<dyn error::Error>> {
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<Pubkey, SignerError> {
Ok(self.pubkey())
}
fn sign_message(&self, _msg: &[u8]) -> Signature {
self.transaction.signatures[0]
}
fn try_sign_message(&self, message: &[u8]) -> Result<Signature, SignerError> {
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::<SystemError>(result, &config)
}
pub fn log_instruction_custom_error<E>(
result: ClientResult<Signature>,
config: &CliConfig,
) -> ProcessResult
where
E: 'static + std::error::Error + DecodeError<E> + 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<Utc> = 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::<Pubkey>().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<Pubkey, SignerError> {
Ok(self.pubkey)
}
fn try_sign_message(&self, _message: &[u8]) -> Result<Signature, SignerError> {
Ok(Signature::new(&[1u8; 64]))
}
}
let mut config = CliConfig::default();
config.output_format = OutputFormat::JsonCompact;
let present: Box<dyn Signer> = Box::new(keypair_from_seed(&[2u8; 32]).unwrap());
let absent: Box<dyn Signer> = Box::new(NullSigner::new(&Pubkey::new(&[3u8; 32])));
let bad: Box<dyn Signer> = 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());
}
}