use crate::{ cli::{ build_balance_message, check_account_for_fee, check_unique_pubkeys, get_blockhash_fee_calculator, log_instruction_custom_error, replace_signatures, required_lamports_from, return_signers, CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult, }, nonce::{check_nonce_account, NONCE_ARG, NONCE_AUTHORITY_ARG}, }; use clap::{App, Arg, ArgMatches, SubCommand}; use console::style; use solana_clap_utils::{input_parsers::*, input_validators::*, ArgConstant}; use solana_client::rpc_client::RpcClient; use solana_sdk::signature::{Keypair, Signature}; use solana_sdk::{ account_utils::State, hash::Hash, pubkey::Pubkey, signature::KeypairUtil, system_instruction::SystemError, sysvar::{ stake_history::{self, StakeHistory}, Sysvar, }, transaction::Transaction, }; use solana_stake_program::stake_state::Meta; use solana_stake_program::{ stake_instruction::{self, StakeError}, stake_state::{Authorized, Lockup, StakeAuthorize, StakeState}, }; use solana_vote_program::vote_state::VoteState; use std::ops::Deref; pub const STAKE_AUTHORITY_ARG: ArgConstant<'static> = ArgConstant { name: "stake_authority", long: "stake-authority", help: "Public key of authorized staker (defaults to cli config pubkey)", }; pub const WITHDRAW_AUTHORITY_ARG: ArgConstant<'static> = ArgConstant { name: "withdraw_authority", long: "withdraw-authority", help: "Public key of authorized withdrawer (defaults to cli config pubkey)", }; fn stake_authority_arg<'a, 'b>() -> Arg<'a, 'b> { Arg::with_name(STAKE_AUTHORITY_ARG.name) .long(STAKE_AUTHORITY_ARG.long) .takes_value(true) .value_name("KEYPAIR") .validator(is_keypair_or_ask_keyword) .help(STAKE_AUTHORITY_ARG.help) } fn withdraw_authority_arg<'a, 'b>() -> Arg<'a, 'b> { Arg::with_name(WITHDRAW_AUTHORITY_ARG.name) .long(WITHDRAW_AUTHORITY_ARG.long) .takes_value(true) .value_name("KEYPAIR") .validator(is_keypair_or_ask_keyword) .help(WITHDRAW_AUTHORITY_ARG.help) } pub trait StakeSubCommands { fn stake_subcommands(self) -> Self; } impl StakeSubCommands for App<'_, '_> { fn stake_subcommands(self) -> Self { self.subcommand( SubCommand::with_name("create-stake-account") .about("Create a stake account") .arg( Arg::with_name("stake_account") .index(1) .value_name("STAKE ACCOUNT") .takes_value(true) .required(true) .validator(is_keypair_or_ask_keyword) .help("Keypair of the stake account to fund") ) .arg( Arg::with_name("amount") .index(2) .value_name("AMOUNT") .takes_value(true) .validator(is_amount) .required(true) .help("The amount of send to the vote account (default unit SOL)") ) .arg( Arg::with_name("unit") .index(3) .value_name("UNIT") .takes_value(true) .possible_values(&["SOL", "lamports"]) .help("Specify unit to use for request") ) .arg( Arg::with_name("custodian") .long("custodian") .value_name("PUBKEY") .takes_value(true) .validator(is_pubkey_or_keypair) .help("Identity of the custodian (can withdraw before lockup expires)") ) .arg( Arg::with_name("lockup_epoch") .long("lockup-epoch") .value_name("EPOCH") .takes_value(true) .help("The epoch height at which this account will be available for withdrawal") ) .arg( Arg::with_name("lockup_date") .long("lockup-date") .value_name("RFC3339 DATE TIME") .validator(is_rfc3339_datetime) .takes_value(true) .help("The date and time at which this account will be available for withdrawal") ) .arg( Arg::with_name(STAKE_AUTHORITY_ARG.name) .long(STAKE_AUTHORITY_ARG.long) .value_name("PUBKEY") .takes_value(true) .validator(is_pubkey_or_keypair) .help(STAKE_AUTHORITY_ARG.help) ) .arg( Arg::with_name(WITHDRAW_AUTHORITY_ARG.name) .long(WITHDRAW_AUTHORITY_ARG.long) .value_name("PUBKEY") .takes_value(true) .validator(is_pubkey_or_keypair) .help(WITHDRAW_AUTHORITY_ARG.help) ) ) .subcommand( SubCommand::with_name("delegate-stake") .about("Delegate stake to a vote account") .arg( Arg::with_name("force") .long("force") .takes_value(false) .hidden(true) // Don't document this argument to discourage its use .help("Override vote account sanity checks (use carefully!)") ) .arg( Arg::with_name("stake_account_pubkey") .index(1) .value_name("STAKE ACCOUNT") .takes_value(true) .required(true) .validator(is_pubkey_or_keypair) .help("Stake account to delegate") ) .arg( Arg::with_name("vote_account_pubkey") .index(2) .value_name("VOTE ACCOUNT") .takes_value(true) .required(true) .validator(is_pubkey_or_keypair) .help("The vote account to which the stake will be delegated") ) .arg(stake_authority_arg()) .arg( Arg::with_name("sign_only") .long("sign-only") .takes_value(false) .help("Sign the transaction offline"), ) .arg( Arg::with_name("signer") .long("signer") .value_name("PUBKEY=BASE58_SIG") .takes_value(true) .validator(is_pubkey_sig) .multiple(true) .help("Provide a public-key/signature pair for the transaction"), ) .arg( Arg::with_name("blockhash") .long("blockhash") .value_name("BLOCKHASH") .takes_value(true) .validator(is_hash) .help("Use the supplied blockhash"), ) .arg( Arg::with_name(NONCE_ARG.name) .long(NONCE_ARG.long) .takes_value(true) .value_name("PUBKEY") .requires("blockhash") .validator(is_pubkey) .help(NONCE_ARG.help) ) .arg( Arg::with_name(NONCE_AUTHORITY_ARG.name) .long(NONCE_AUTHORITY_ARG.long) .takes_value(true) .requires(NONCE_ARG.name) .validator(is_keypair_or_ask_keyword) .help(NONCE_AUTHORITY_ARG.help) ), ) .subcommand( SubCommand::with_name("stake-authorize-staker") .about("Authorize a new stake signing keypair for the given stake account") .arg( Arg::with_name("stake_account_pubkey") .index(1) .value_name("STAKE ACCOUNT") .takes_value(true) .required(true) .validator(is_pubkey_or_keypair) .help("Stake account in which to set the authorized staker") ) .arg( Arg::with_name("authorized_pubkey") .index(2) .value_name("AUTHORIZE PUBKEY") .takes_value(true) .required(true) .validator(is_pubkey_or_keypair) .help("New authorized staker") ) .arg(stake_authority_arg()) ) .subcommand( SubCommand::with_name("stake-authorize-withdrawer") .about("Authorize a new withdraw signing keypair for the given stake account") .arg( Arg::with_name("stake_account_pubkey") .index(1) .value_name("STAKE ACCOUNT") .takes_value(true) .required(true) .validator(is_pubkey_or_keypair) .help("Stake account in which to set the authorized withdrawer") ) .arg( Arg::with_name("authorized_pubkey") .index(2) .value_name("AUTHORIZE PUBKEY") .takes_value(true) .required(true) .validator(is_pubkey_or_keypair) .help("New authorized withdrawer") ) .arg(withdraw_authority_arg()) ) .subcommand( SubCommand::with_name("deactivate-stake") .about("Deactivate the delegated stake from the stake account") .arg( Arg::with_name("stake_account_pubkey") .index(1) .value_name("STAKE ACCOUNT") .takes_value(true) .required(true) .help("Stake account to be deactivated.") ) .arg(stake_authority_arg()) .arg( Arg::with_name("sign_only") .long("sign-only") .takes_value(false) .help("Sign the transaction offline"), ) .arg( Arg::with_name("signer") .long("signer") .value_name("PUBKEY=BASE58_SIG") .takes_value(true) .validator(is_pubkey_sig) .multiple(true) .help("Provide a public-key/signature pair for the transaction"), ) .arg( Arg::with_name("blockhash") .long("blockhash") .value_name("BLOCKHASH") .takes_value(true) .validator(is_hash) .help("Use the supplied blockhash"), ) .arg( Arg::with_name(NONCE_ARG.name) .long(NONCE_ARG.long) .takes_value(true) .value_name("PUBKEY") .requires("blockhash") .validator(is_pubkey) .help(NONCE_ARG.help) ) .arg( Arg::with_name(NONCE_AUTHORITY_ARG.name) .long(NONCE_AUTHORITY_ARG.long) .takes_value(true) .requires(NONCE_ARG.name) .validator(is_keypair_or_ask_keyword) .help(NONCE_AUTHORITY_ARG.help) ), ) .subcommand( SubCommand::with_name("withdraw-stake") .about("Withdraw the unstaked lamports from the stake account") .arg( Arg::with_name("stake_account_pubkey") .index(1) .value_name("STAKE ACCOUNT") .takes_value(true) .required(true) .validator(is_pubkey_or_keypair) .help("Stake account from which to withdraw") ) .arg( Arg::with_name("destination_account_pubkey") .index(2) .value_name("DESTINATION ACCOUNT") .takes_value(true) .required(true) .validator(is_pubkey_or_keypair) .help("The account to which the lamports should be transferred") ) .arg( Arg::with_name("amount") .index(3) .value_name("AMOUNT") .takes_value(true) .validator(is_amount) .required(true) .help("The amount to withdraw from the stake account (default unit SOL)") ) .arg( Arg::with_name("unit") .index(4) .value_name("UNIT") .takes_value(true) .possible_values(&["SOL", "lamports"]) .help("Specify unit to use for request") ) .arg(withdraw_authority_arg()) ) .subcommand( SubCommand::with_name("redeem-vote-credits") .about("Redeem credits in the stake account") .arg( Arg::with_name("stake_account_pubkey") .index(1) .value_name("STAKE ACCOUNT") .takes_value(true) .required(true) .validator(is_pubkey_or_keypair) .help("Address of the stake account in which to redeem credits") ) .arg( Arg::with_name("vote_account_pubkey") .index(2) .value_name("VOTE ACCOUNT") .takes_value(true) .required(true) .validator(is_pubkey_or_keypair) .help("The vote account to which the stake is currently delegated.") ) ) .subcommand( SubCommand::with_name("show-stake-account") .about("Show the contents of a stake account") .arg( Arg::with_name("stake_account_pubkey") .index(1) .value_name("STAKE ACCOUNT") .takes_value(true) .required(true) .validator(is_pubkey_or_keypair) .help("Address of the stake account to display") ) .arg( Arg::with_name("lamports") .long("lamports") .takes_value(false) .help("Display balance in lamports instead of SOL") ) ) .subcommand( SubCommand::with_name("show-stake-history") .about("Show the stake history") .arg( Arg::with_name("lamports") .long("lamports") .takes_value(false) .help("Display balance in lamports instead of SOL") ) ) } } pub fn parse_stake_create_account(matches: &ArgMatches<'_>) -> Result { let stake_account = keypair_of(matches, "stake_account").unwrap(); let epoch = value_of(&matches, "lockup_epoch").unwrap_or(0); let unix_timestamp = unix_timestamp_from_rfc3339_datetime(&matches, "lockup_date").unwrap_or(0); let custodian = pubkey_of(matches, "custodian").unwrap_or_default(); let staker = pubkey_of(matches, STAKE_AUTHORITY_ARG.name); let withdrawer = pubkey_of(matches, WITHDRAW_AUTHORITY_ARG.name); let lamports = required_lamports_from(matches, "amount", "unit")?; Ok(CliCommandInfo { command: CliCommand::CreateStakeAccount { stake_account: stake_account.into(), staker, withdrawer, lockup: Lockup { custodian, epoch, unix_timestamp, }, lamports, }, require_keypair: true, }) } pub fn parse_stake_delegate_stake(matches: &ArgMatches<'_>) -> Result { let stake_account_pubkey = pubkey_of(matches, "stake_account_pubkey").unwrap(); let vote_account_pubkey = pubkey_of(matches, "vote_account_pubkey").unwrap(); let stake_authority = if matches.is_present(STAKE_AUTHORITY_ARG.name) { let authority = keypair_of(&matches, STAKE_AUTHORITY_ARG.name) .ok_or_else(|| CliError::BadParameter("Invalid keypair for stake-authority".into()))?; Some(authority.into()) } else { None }; let force = matches.is_present("force"); let sign_only = matches.is_present("sign_only"); let signers = pubkeys_sigs_of(&matches, "signer"); let blockhash = value_of(matches, "blockhash"); let require_keypair = signers.is_none(); let nonce_account = pubkey_of(&matches, NONCE_ARG.name); let nonce_authority = if matches.is_present(NONCE_AUTHORITY_ARG.name) { let authority = keypair_of(&matches, NONCE_AUTHORITY_ARG.name) .ok_or_else(|| CliError::BadParameter("Invalid keypair for nonce-authority".into()))?; Some(authority.into()) } else { None }; Ok(CliCommandInfo { command: CliCommand::DelegateStake { stake_account_pubkey, vote_account_pubkey, stake_authority, force, sign_only, signers, blockhash, nonce_account, nonce_authority, }, require_keypair, }) } pub fn parse_stake_authorize( matches: &ArgMatches<'_>, stake_authorize: StakeAuthorize, ) -> Result { let stake_account_pubkey = pubkey_of(matches, "stake_account_pubkey").unwrap(); let new_authorized_pubkey = pubkey_of(matches, "authorized_pubkey").unwrap(); let authority_flag = match stake_authorize { StakeAuthorize::Staker => STAKE_AUTHORITY_ARG.name, StakeAuthorize::Withdrawer => WITHDRAW_AUTHORITY_ARG.name, }; let authority = if matches.is_present(authority_flag) { let authority = keypair_of(&matches, authority_flag).ok_or_else(|| { CliError::BadParameter(format!("Invalid keypair for {}", authority_flag)) })?; Some(authority.into()) } else { None }; Ok(CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, new_authorized_pubkey, stake_authorize, authority, }, require_keypair: true, }) } pub fn parse_redeem_vote_credits(matches: &ArgMatches<'_>) -> Result { let stake_account_pubkey = pubkey_of(matches, "stake_account_pubkey").unwrap(); let vote_account_pubkey = pubkey_of(matches, "vote_account_pubkey").unwrap(); Ok(CliCommandInfo { command: CliCommand::RedeemVoteCredits(stake_account_pubkey, vote_account_pubkey), require_keypair: true, }) } pub fn parse_stake_deactivate_stake(matches: &ArgMatches<'_>) -> Result { let stake_account_pubkey = pubkey_of(matches, "stake_account_pubkey").unwrap(); let stake_authority = if matches.is_present(STAKE_AUTHORITY_ARG.name) { let authority = keypair_of(&matches, STAKE_AUTHORITY_ARG.name) .ok_or_else(|| CliError::BadParameter("Invalid keypair for stake-authority".into()))?; Some(authority.into()) } else { None }; let sign_only = matches.is_present("sign_only"); let signers = pubkeys_sigs_of(&matches, "signer"); let blockhash = value_of(matches, "blockhash"); let require_keypair = signers.is_none(); let nonce_account = pubkey_of(&matches, NONCE_ARG.name); let nonce_authority = if matches.is_present(NONCE_AUTHORITY_ARG.name) { let authority = keypair_of(&matches, NONCE_AUTHORITY_ARG.name) .ok_or_else(|| CliError::BadParameter("Invalid keypair for nonce-authority".into()))?; Some(authority.into()) } else { None }; Ok(CliCommandInfo { command: CliCommand::DeactivateStake { stake_account_pubkey, stake_authority, sign_only, signers, blockhash, nonce_account, nonce_authority, }, require_keypair, }) } pub fn parse_stake_withdraw_stake(matches: &ArgMatches<'_>) -> Result { let stake_account_pubkey = pubkey_of(matches, "stake_account_pubkey").unwrap(); let destination_account_pubkey = pubkey_of(matches, "destination_account_pubkey").unwrap(); let lamports = required_lamports_from(matches, "amount", "unit")?; let withdraw_authority = if matches.is_present(WITHDRAW_AUTHORITY_ARG.name) { let authority = keypair_of(&matches, WITHDRAW_AUTHORITY_ARG.name).ok_or_else(|| { CliError::BadParameter("Invalid keypair for withdraw-authority".into()) })?; Some(authority.into()) } else { None }; Ok(CliCommandInfo { command: CliCommand::WithdrawStake { stake_account_pubkey, destination_account_pubkey, lamports, withdraw_authority, }, require_keypair: true, }) } pub fn parse_show_stake_account(matches: &ArgMatches<'_>) -> Result { let stake_account_pubkey = pubkey_of(matches, "stake_account_pubkey").unwrap(); let use_lamports_unit = matches.is_present("lamports"); Ok(CliCommandInfo { command: CliCommand::ShowStakeAccount { pubkey: stake_account_pubkey, use_lamports_unit, }, require_keypair: false, }) } pub fn parse_show_stake_history(matches: &ArgMatches<'_>) -> Result { let use_lamports_unit = matches.is_present("lamports"); Ok(CliCommandInfo { command: CliCommand::ShowStakeHistory { use_lamports_unit }, require_keypair: false, }) } pub fn process_create_stake_account( rpc_client: &RpcClient, config: &CliConfig, stake_account: &Keypair, staker: &Option, withdrawer: &Option, lockup: &Lockup, lamports: u64, ) -> ProcessResult { let stake_account_pubkey = stake_account.pubkey(); check_unique_pubkeys( (&config.keypair.pubkey(), "cli keypair".to_string()), (&stake_account_pubkey, "stake_account_pubkey".to_string()), )?; if let Ok(stake_account) = rpc_client.get_account(&stake_account_pubkey) { let err_msg = if stake_account.owner == solana_stake_program::id() { format!("Stake account {} already exists", stake_account_pubkey) } else { format!( "Account {} already exists and is not a stake account", stake_account_pubkey ) }; return Err(CliError::BadParameter(err_msg).into()); } let minimum_balance = rpc_client.get_minimum_balance_for_rent_exemption(std::mem::size_of::())?; if lamports < minimum_balance { return Err(CliError::BadParameter(format!( "need atleast {} lamports for stake account to be rent exempt, provided lamports: {}", minimum_balance, lamports )) .into()); } let authorized = Authorized { staker: staker.unwrap_or(config.keypair.pubkey()), withdrawer: withdrawer.unwrap_or(config.keypair.pubkey()), }; let ixs = stake_instruction::create_account( &config.keypair.pubkey(), &stake_account_pubkey, &authorized, lockup, lamports, ); let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; let mut tx = Transaction::new_signed_with_payer( ixs, Some(&config.keypair.pubkey()), &[&config.keypair, stake_account], recent_blockhash, ); check_account_for_fee( rpc_client, &config.keypair.pubkey(), &fee_calculator, &tx.message, )?; let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair, stake_account]); log_instruction_custom_error::(result) } pub fn process_stake_authorize( rpc_client: &RpcClient, config: &CliConfig, stake_account_pubkey: &Pubkey, authorized_pubkey: &Pubkey, stake_authorize: StakeAuthorize, authority: Option<&Keypair>, ) -> ProcessResult { check_unique_pubkeys( (stake_account_pubkey, "stake_account_pubkey".to_string()), (authorized_pubkey, "new_authorized_pubkey".to_string()), )?; let authority = authority.unwrap_or(&config.keypair); let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; let ixs = vec![stake_instruction::authorize( stake_account_pubkey, // stake account to update &authority.pubkey(), // currently authorized authorized_pubkey, // new stake signer stake_authorize, // stake or withdraw )]; let mut tx = Transaction::new_signed_with_payer( ixs, Some(&config.keypair.pubkey()), &[&config.keypair, authority], recent_blockhash, ); check_account_for_fee( rpc_client, &config.keypair.pubkey(), &fee_calculator, &tx.message, )?; let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair]); log_instruction_custom_error::(result) } pub fn process_deactivate_stake_account( rpc_client: &RpcClient, config: &CliConfig, stake_account_pubkey: &Pubkey, stake_authority: Option<&Keypair>, sign_only: bool, signers: &Option>, blockhash: Option, nonce_account: Option, nonce_authority: Option<&Keypair>, ) -> ProcessResult { let (recent_blockhash, fee_calculator) = get_blockhash_fee_calculator(rpc_client, sign_only, blockhash)?; let stake_authority = stake_authority.unwrap_or(&config.keypair); let ixs = vec![stake_instruction::deactivate_stake( stake_account_pubkey, &stake_authority.pubkey(), )]; let mut tx = if let Some(nonce_account) = &nonce_account { let nonce_authority: &Keypair = nonce_authority.unwrap_or(&config.keypair); Transaction::new_signed_with_nonce( ixs, Some(&config.keypair.pubkey()), &[&config.keypair, nonce_authority, stake_authority], nonce_account, &nonce_authority.pubkey(), recent_blockhash, ) } else { Transaction::new_signed_with_payer( ixs, Some(&config.keypair.pubkey()), &[&config.keypair, stake_authority], recent_blockhash, ) }; if let Some(signers) = signers { replace_signatures(&mut tx, &signers)?; } if sign_only { return_signers(&tx) } else { if let Some(nonce_account) = &nonce_account { let nonce_authority: &Keypair = nonce_authority.unwrap_or(&config.keypair); let nonce_account = rpc_client.get_account(nonce_account)?; check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?; } check_account_for_fee( rpc_client, &tx.message.account_keys[0], &fee_calculator, &tx.message, )?; let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair]); log_instruction_custom_error::(result) } } pub fn process_withdraw_stake( rpc_client: &RpcClient, config: &CliConfig, stake_account_pubkey: &Pubkey, destination_account_pubkey: &Pubkey, lamports: u64, withdraw_authority: Option<&Keypair>, ) -> ProcessResult { let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; let withdraw_authority = withdraw_authority.unwrap_or(&config.keypair); let ixs = vec![stake_instruction::withdraw( stake_account_pubkey, &withdraw_authority.pubkey(), destination_account_pubkey, lamports, )]; let mut tx = Transaction::new_signed_with_payer( ixs, Some(&config.keypair.pubkey()), &[&config.keypair, withdraw_authority], recent_blockhash, ); check_account_for_fee( rpc_client, &config.keypair.pubkey(), &fee_calculator, &tx.message, )?; let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair]); log_instruction_custom_error::(result) } pub fn process_redeem_vote_credits( rpc_client: &RpcClient, config: &CliConfig, stake_account_pubkey: &Pubkey, vote_account_pubkey: &Pubkey, ) -> ProcessResult { let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; let ixs = vec![stake_instruction::redeem_vote_credits( stake_account_pubkey, vote_account_pubkey, )]; let mut tx = Transaction::new_signed_with_payer( ixs, Some(&config.keypair.pubkey()), &[&config.keypair], recent_blockhash, ); check_account_for_fee( rpc_client, &config.keypair.pubkey(), &fee_calculator, &tx.message, )?; let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair]); log_instruction_custom_error::(result) } pub fn process_show_stake_account( rpc_client: &RpcClient, _config: &CliConfig, stake_account_pubkey: &Pubkey, use_lamports_unit: bool, ) -> ProcessResult { let stake_account = rpc_client.get_account(stake_account_pubkey)?; if stake_account.owner != solana_stake_program::id() { return Err(CliError::RpcRequestError(format!( "{:?} is not a stake account", stake_account_pubkey )) .into()); } fn show_authorized(authorized: &Authorized) { println!("authorized staker: {}", authorized.staker); println!("authorized withdrawer: {}", authorized.staker); } fn show_lockup(lockup: &Lockup) { println!("lockup epoch: {}", lockup.epoch); println!("lockup custodian: {}", lockup.custodian); } match stake_account.state() { Ok(StakeState::Stake( Meta { authorized, lockup, .. }, stake, )) => { println!( "total stake: {}", build_balance_message(stake_account.lamports, use_lamports_unit, true) ); println!("credits observed: {}", stake.credits_observed); println!( "delegated stake: {}", build_balance_message(stake.delegation.stake, use_lamports_unit, true) ); if stake.delegation.voter_pubkey != Pubkey::default() { println!("delegated voter pubkey: {}", stake.delegation.voter_pubkey); } println!( "stake activates starting from epoch: {}", if stake.delegation.activation_epoch < std::u64::MAX { stake.delegation.activation_epoch } else { 0 } ); if stake.delegation.deactivation_epoch < std::u64::MAX { println!( "stake deactivates starting from epoch: {}", stake.delegation.deactivation_epoch ); } show_authorized(&authorized); show_lockup(&lockup); Ok("".to_string()) } Ok(StakeState::RewardsPool) => Ok("Stake account is a rewards pool".to_string()), Ok(StakeState::Uninitialized) => Ok("Stake account is uninitialized".to_string()), Ok(StakeState::Initialized(Meta { authorized, lockup, .. })) => { println!("Stake account is undelegated"); show_authorized(&authorized); show_lockup(&lockup); Ok("".to_string()) } Err(err) => Err(CliError::RpcRequestError(format!( "Account data could not be deserialized to stake state: {:?}", err )) .into()), } } pub fn process_show_stake_history( rpc_client: &RpcClient, _config: &CliConfig, use_lamports_unit: bool, ) -> ProcessResult { let stake_history_account = rpc_client.get_account(&stake_history::id())?; let stake_history = StakeHistory::from_account(&stake_history_account).ok_or_else(|| { CliError::RpcRequestError("Failed to deserialize stake history".to_string()) })?; println!(); println!( "{}", style(format!( " {:<5} {:>20} {:>20} {:>20}", "Epoch", "Effective Stake", "Activating Stake", "Deactivating Stake", )) .bold() ); for (epoch, entry) in stake_history.deref() { println!( " {:>5} {:>20} {:>20} {:>20} {}", epoch, build_balance_message(entry.effective, use_lamports_unit, false), build_balance_message(entry.activating, use_lamports_unit, false), build_balance_message(entry.deactivating, use_lamports_unit, false), if use_lamports_unit { "lamports" } else { "SOL" } ); } Ok("".to_string()) } #[allow(clippy::too_many_arguments)] pub fn process_delegate_stake( rpc_client: &RpcClient, config: &CliConfig, stake_account_pubkey: &Pubkey, vote_account_pubkey: &Pubkey, stake_authority: Option<&Keypair>, force: bool, sign_only: bool, signers: &Option>, blockhash: Option, nonce_account: Option, nonce_authority: Option<&Keypair>, ) -> ProcessResult { check_unique_pubkeys( (&config.keypair.pubkey(), "cli keypair".to_string()), (stake_account_pubkey, "stake_account_pubkey".to_string()), )?; let stake_authority = stake_authority.unwrap_or(&config.keypair); // Sanity check the vote account to ensure it is attached to a validator that has recently // voted at the tip of the ledger let vote_account_data = rpc_client .get_account_data(vote_account_pubkey) .map_err(|_| { CliError::RpcRequestError(format!("Vote account not found: {}", vote_account_pubkey)) })?; let vote_state = VoteState::deserialize(&vote_account_data).map_err(|_| { CliError::RpcRequestError( "Account data could not be deserialized to vote state".to_string(), ) })?; let sanity_check_result = match vote_state.root_slot { None => Err(CliError::BadParameter( "Unable to delegate. Vote account has no root slot".to_string(), )), Some(root_slot) => { let slot = rpc_client.get_slot()?; if root_slot + solana_sdk::clock::DEFAULT_SLOTS_PER_TURN < slot { Err(CliError::BadParameter( format!( "Unable to delegate. Vote account root slot ({}) is too old, the current slot is {}", root_slot, slot ) )) } else { Ok(()) } } }; if sanity_check_result.is_err() { if !force { sanity_check_result?; } else { println!("--force supplied, ignoring: {:?}", sanity_check_result); } } let (recent_blockhash, fee_calculator) = get_blockhash_fee_calculator(rpc_client, sign_only, blockhash)?; let ixs = vec![stake_instruction::delegate_stake( stake_account_pubkey, &stake_authority.pubkey(), vote_account_pubkey, )]; let mut tx = if let Some(nonce_account) = &nonce_account { let nonce_authority: &Keypair = nonce_authority.unwrap_or(&config.keypair); Transaction::new_signed_with_nonce( ixs, Some(&config.keypair.pubkey()), &[&config.keypair, nonce_authority, stake_authority], nonce_account, &nonce_authority.pubkey(), recent_blockhash, ) } else { Transaction::new_signed_with_payer( ixs, Some(&config.keypair.pubkey()), &[&config.keypair, stake_authority], recent_blockhash, ) }; if let Some(signers) = signers { replace_signatures(&mut tx, &signers)?; } if sign_only { return_signers(&tx) } else { if let Some(nonce_account) = &nonce_account { let nonce_authority: &Keypair = nonce_authority.unwrap_or(&config.keypair); let nonce_account = rpc_client.get_account(nonce_account)?; check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?; } check_account_for_fee( rpc_client, &tx.message.account_keys[0], &fee_calculator, &tx.message, )?; let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair]); log_instruction_custom_error::(result) } } #[cfg(test)] mod tests { use super::*; use crate::cli::{app, parse_command}; use solana_sdk::signature::{read_keypair_file, write_keypair}; use tempfile::NamedTempFile; fn make_tmp_file() -> (String, NamedTempFile) { let tmp_file = NamedTempFile::new().unwrap(); (String::from(tmp_file.path().to_str().unwrap()), tmp_file) } fn parse_authorize_tests( test_commands: &App, stake_account_pubkey: Pubkey, authority_keypair_file: &str, stake_authorize: StakeAuthorize, ) { let stake_account_string = stake_account_pubkey.to_string(); let (subcommand, authority_flag) = match stake_authorize { StakeAuthorize::Staker => ("stake-authorize-staker", "--stake-authority"), StakeAuthorize::Withdrawer => ("stake-authorize-withdrawer", "--withdraw-authority"), }; // Test Staker Subcommand let test_authorize = test_commands.clone().get_matches_from(vec![ "test", &subcommand, &stake_account_string, &stake_account_string, ]); assert_eq!( parse_command(&test_authorize).unwrap(), CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, new_authorized_pubkey: stake_account_pubkey, stake_authorize, authority: None, }, require_keypair: true } ); // Test Staker Subcommand w/ authority let test_authorize = test_commands.clone().get_matches_from(vec![ "test", &subcommand, &stake_account_string, &stake_account_string, &authority_flag, &authority_keypair_file, ]); assert_eq!( parse_command(&test_authorize).unwrap(), CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, new_authorized_pubkey: stake_account_pubkey, stake_authorize, authority: Some(read_keypair_file(&authority_keypair_file).unwrap().into()), }, require_keypair: true } ); } #[test] fn test_parse_command() { let test_commands = app("test", "desc", "version"); let (keypair_file, mut tmp_file) = make_tmp_file(); let stake_account_keypair = Keypair::new(); write_keypair(&stake_account_keypair, tmp_file.as_file_mut()).unwrap(); let stake_account_pubkey = stake_account_keypair.pubkey(); let (stake_authority_keypair_file, mut tmp_file) = make_tmp_file(); let stake_authority_keypair = Keypair::new(); write_keypair(&stake_authority_keypair, tmp_file.as_file_mut()).unwrap(); parse_authorize_tests( &test_commands, stake_account_pubkey, &stake_authority_keypair_file, StakeAuthorize::Staker, ); parse_authorize_tests( &test_commands, stake_account_pubkey, &stake_authority_keypair_file, StakeAuthorize::Withdrawer, ); // Test CreateStakeAccount SubCommand let custodian = Pubkey::new_rand(); let custodian_string = format!("{}", custodian); let authorized = Pubkey::new_rand(); let authorized_string = format!("{}", authorized); let test_create_stake_account = test_commands.clone().get_matches_from(vec![ "test", "create-stake-account", &keypair_file, "50", "--stake-authority", &authorized_string, "--withdraw-authority", &authorized_string, "--custodian", &custodian_string, "--lockup-epoch", "43", "lamports", ]); assert_eq!( parse_command(&test_create_stake_account).unwrap(), CliCommandInfo { command: CliCommand::CreateStakeAccount { stake_account: stake_account_keypair.into(), staker: Some(authorized), withdrawer: Some(authorized), lockup: Lockup { epoch: 43, unix_timestamp: 0, custodian, }, lamports: 50 }, require_keypair: true } ); let (keypair_file, mut tmp_file) = make_tmp_file(); let stake_account_keypair = Keypair::new(); write_keypair(&stake_account_keypair, tmp_file.as_file_mut()).unwrap(); let stake_account_pubkey = stake_account_keypair.pubkey(); let stake_account_string = stake_account_pubkey.to_string(); let test_create_stake_account2 = test_commands.clone().get_matches_from(vec![ "test", "create-stake-account", &keypair_file, "50", "lamports", ]); assert_eq!( parse_command(&test_create_stake_account2).unwrap(), CliCommandInfo { command: CliCommand::CreateStakeAccount { stake_account: stake_account_keypair.into(), staker: None, withdrawer: None, lockup: Lockup::default(), lamports: 50 }, require_keypair: true } ); // Test DelegateStake Subcommand let vote_account_pubkey = Pubkey::new_rand(); let vote_account_string = vote_account_pubkey.to_string(); let test_delegate_stake = test_commands.clone().get_matches_from(vec![ "test", "delegate-stake", &stake_account_string, &vote_account_string, ]); assert_eq!( parse_command(&test_delegate_stake).unwrap(), CliCommandInfo { command: CliCommand::DelegateStake { stake_account_pubkey, vote_account_pubkey, stake_authority: None, force: false, sign_only: false, signers: None, blockhash: None, nonce_account: None, nonce_authority: None, }, require_keypair: true } ); // Test DelegateStake Subcommand w/ authority let vote_account_pubkey = Pubkey::new_rand(); let vote_account_string = vote_account_pubkey.to_string(); let test_delegate_stake = test_commands.clone().get_matches_from(vec![ "test", "delegate-stake", &stake_account_string, &vote_account_string, "--stake-authority", &stake_authority_keypair_file, ]); assert_eq!( parse_command(&test_delegate_stake).unwrap(), CliCommandInfo { command: CliCommand::DelegateStake { stake_account_pubkey, vote_account_pubkey, stake_authority: Some( read_keypair_file(&stake_authority_keypair_file) .unwrap() .into() ), force: false, sign_only: false, signers: None, blockhash: None, nonce_account: None, nonce_authority: None, }, require_keypair: true } ); // Test DelegateStake Subcommand w/ force let test_delegate_stake = test_commands.clone().get_matches_from(vec![ "test", "delegate-stake", "--force", &stake_account_string, &vote_account_string, ]); assert_eq!( parse_command(&test_delegate_stake).unwrap(), CliCommandInfo { command: CliCommand::DelegateStake { stake_account_pubkey, vote_account_pubkey, stake_authority: None, force: true, sign_only: false, signers: None, blockhash: None, nonce_account: None, nonce_authority: None, }, require_keypair: true } ); // Test Delegate Subcommand w/ Blockhash let blockhash = Hash::default(); let blockhash_string = format!("{}", blockhash); let test_delegate_stake = test_commands.clone().get_matches_from(vec![ "test", "delegate-stake", &stake_account_string, &vote_account_string, "--blockhash", &blockhash_string, ]); assert_eq!( parse_command(&test_delegate_stake).unwrap(), CliCommandInfo { command: CliCommand::DelegateStake { stake_account_pubkey, vote_account_pubkey, stake_authority: None, force: false, sign_only: false, signers: None, blockhash: Some(blockhash), nonce_account: None, nonce_authority: None, }, require_keypair: true } ); let test_delegate_stake = test_commands.clone().get_matches_from(vec![ "test", "delegate-stake", &stake_account_string, &vote_account_string, "--sign-only", ]); assert_eq!( parse_command(&test_delegate_stake).unwrap(), CliCommandInfo { command: CliCommand::DelegateStake { stake_account_pubkey, vote_account_pubkey, stake_authority: None, force: false, sign_only: true, signers: None, blockhash: None, nonce_account: None, nonce_authority: None, }, require_keypair: true } ); // Test Delegate Subcommand w/ signer let key1 = Pubkey::new_rand(); let sig1 = Keypair::new().sign_message(&[0u8]); let signer1 = format!("{}={}", key1, sig1); let test_delegate_stake = test_commands.clone().get_matches_from(vec![ "test", "delegate-stake", &stake_account_string, &vote_account_string, "--signer", &signer1, ]); assert_eq!( parse_command(&test_delegate_stake).unwrap(), CliCommandInfo { command: CliCommand::DelegateStake { stake_account_pubkey, vote_account_pubkey, stake_authority: None, force: false, sign_only: false, signers: Some(vec![(key1, sig1)]), blockhash: None, nonce_account: None, nonce_authority: None, }, require_keypair: false } ); // Test Delegate Subcommand w/ signers let key2 = Pubkey::new_rand(); let sig2 = Keypair::new().sign_message(&[0u8]); let signer2 = format!("{}={}", key2, sig2); let test_delegate_stake = test_commands.clone().get_matches_from(vec![ "test", "delegate-stake", &stake_account_string, &vote_account_string, "--signer", &signer1, "--signer", &signer2, ]); assert_eq!( parse_command(&test_delegate_stake).unwrap(), CliCommandInfo { command: CliCommand::DelegateStake { stake_account_pubkey, vote_account_pubkey, stake_authority: None, force: false, sign_only: false, signers: Some(vec![(key1, sig1), (key2, sig2)]), blockhash: None, nonce_account: None, nonce_authority: None, }, require_keypair: false } ); // Test WithdrawStake Subcommand let test_withdraw_stake = test_commands.clone().get_matches_from(vec![ "test", "withdraw-stake", &stake_account_string, &stake_account_string, "42", "lamports", ]); assert_eq!( parse_command(&test_withdraw_stake).unwrap(), CliCommandInfo { command: CliCommand::WithdrawStake { stake_account_pubkey, destination_account_pubkey: stake_account_pubkey, lamports: 42, withdraw_authority: None, }, require_keypair: true } ); // Test WithdrawStake Subcommand w/ authority let test_withdraw_stake = test_commands.clone().get_matches_from(vec![ "test", "withdraw-stake", &stake_account_string, &stake_account_string, "42", "lamports", "--withdraw-authority", &stake_authority_keypair_file, ]); assert_eq!( parse_command(&test_withdraw_stake).unwrap(), CliCommandInfo { command: CliCommand::WithdrawStake { stake_account_pubkey, destination_account_pubkey: stake_account_pubkey, lamports: 42, withdraw_authority: Some( read_keypair_file(&stake_authority_keypair_file) .unwrap() .into() ), }, require_keypair: true } ); // Test DeactivateStake Subcommand let test_deactivate_stake = test_commands.clone().get_matches_from(vec![ "test", "deactivate-stake", &stake_account_string, ]); assert_eq!( parse_command(&test_deactivate_stake).unwrap(), CliCommandInfo { command: CliCommand::DeactivateStake { stake_account_pubkey, stake_authority: None, sign_only: false, signers: None, blockhash: None, nonce_account: None, nonce_authority: None, }, require_keypair: true } ); // Test DeactivateStake Subcommand w/ authority let test_deactivate_stake = test_commands.clone().get_matches_from(vec![ "test", "deactivate-stake", &stake_account_string, "--stake-authority", &stake_authority_keypair_file, ]); assert_eq!( parse_command(&test_deactivate_stake).unwrap(), CliCommandInfo { command: CliCommand::DeactivateStake { stake_account_pubkey, stake_authority: Some( read_keypair_file(&stake_authority_keypair_file) .unwrap() .into() ), sign_only: false, signers: None, blockhash: None, nonce_account: None, nonce_authority: None, }, require_keypair: true } ); // Test Deactivate Subcommand w/ Blockhash let blockhash = Hash::default(); let blockhash_string = format!("{}", blockhash); let test_deactivate_stake = test_commands.clone().get_matches_from(vec![ "test", "deactivate-stake", &stake_account_string, "--blockhash", &blockhash_string, ]); assert_eq!( parse_command(&test_deactivate_stake).unwrap(), CliCommandInfo { command: CliCommand::DeactivateStake { stake_account_pubkey, stake_authority: None, sign_only: false, signers: None, blockhash: Some(blockhash), nonce_account: None, nonce_authority: None, }, require_keypair: true } ); let test_deactivate_stake = test_commands.clone().get_matches_from(vec![ "test", "deactivate-stake", &stake_account_string, "--sign-only", ]); assert_eq!( parse_command(&test_deactivate_stake).unwrap(), CliCommandInfo { command: CliCommand::DeactivateStake { stake_account_pubkey, stake_authority: None, sign_only: true, signers: None, blockhash: None, nonce_account: None, nonce_authority: None, }, require_keypair: true } ); // Test Deactivate Subcommand w/ signers let key1 = Pubkey::new_rand(); let sig1 = Keypair::new().sign_message(&[0u8]); let signer1 = format!("{}={}", key1, sig1); let test_deactivate_stake = test_commands.clone().get_matches_from(vec![ "test", "deactivate-stake", &stake_account_string, "--signer", &signer1, ]); assert_eq!( parse_command(&test_deactivate_stake).unwrap(), CliCommandInfo { command: CliCommand::DeactivateStake { stake_account_pubkey, stake_authority: None, sign_only: false, signers: Some(vec![(key1, sig1)]), blockhash: None, nonce_account: None, nonce_authority: None, }, require_keypair: false } ); // Test Deactivate Subcommand w/ signers let key2 = Pubkey::new_rand(); let sig2 = Keypair::new().sign_message(&[0u8]); let signer2 = format!("{}={}", key2, sig2); let test_deactivate_stake = test_commands.clone().get_matches_from(vec![ "test", "deactivate-stake", &stake_account_string, "--signer", &signer1, "--signer", &signer2, ]); assert_eq!( parse_command(&test_deactivate_stake).unwrap(), CliCommandInfo { command: CliCommand::DeactivateStake { stake_account_pubkey, stake_authority: None, sign_only: false, signers: Some(vec![(key1, sig1), (key2, sig2)]), blockhash: None, nonce_account: None, nonce_authority: None, }, require_keypair: false } ); } }