diff --git a/clap-utils/src/input_parsers.rs b/clap-utils/src/input_parsers.rs index d0a7315dfb..203ebb5060 100644 --- a/clap-utils/src/input_parsers.rs +++ b/clap-utils/src/input_parsers.rs @@ -89,6 +89,7 @@ mod tests { .multiple(true), ) .arg(Arg::with_name("single").takes_value(true).long("single")) + .arg(Arg::with_name("unit").takes_value(true).long("unit")) } fn tmp_file_path(name: &str, pubkey: &Pubkey) -> String { @@ -208,4 +209,18 @@ mod tests { Some(vec![(key1, sig1), (key2, sig2)]) ); } + + #[test] + fn test_amount_of() { + let matches = app() + .clone() + .get_matches_from(vec!["test", "--single", "50", "--unit", "lamports"]); + assert_eq!(amount_of(&matches, "single", "unit"), Some(50)); + let matches = app() + .clone() + .get_matches_from(vec!["test", "--single", "50", "--unit", "SOL"]); + assert_eq!(amount_of(&matches, "single", "unit"), Some(50000000000)); + assert_eq!(amount_of(&matches, "multiple", "unit"), None); + assert_eq!(amount_of(&matches, "multiple", "unit"), None); + } } diff --git a/clap-utils/src/input_validators.rs b/clap-utils/src/input_validators.rs index 81ac025cbc..8121e06e56 100644 --- a/clap-utils/src/input_validators.rs +++ b/clap-utils/src/input_validators.rs @@ -118,3 +118,10 @@ pub fn is_valid_percentage(percentage: String) -> Result<(), String> { } }) } + +pub fn is_amount(amount: String) -> Result<(), String> { + amount + .parse::() + .map(|_| ()) + .map_err(|e| format!("{:?}", e)) +} diff --git a/cli/src/cli.rs b/cli/src/cli.rs index ce15dfa5bc..064d193e98 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -1,6 +1,7 @@ use crate::{ cluster_query::*, display::{println_name_value, println_signers}, + nonce::*, stake::*, storage::*, validator_info::*, @@ -104,6 +105,22 @@ pub enum CliCommand { ShowValidators { use_lamports_unit: bool, }, + // Nonce commands + CreateNonceAccount { + nonce_account: KeypairEq, + lamports: u64, + }, + GetNonce(Pubkey), + NewNonce(KeypairEq), + ShowNonceAccount { + nonce_account_pubkey: Pubkey, + use_lamports_unit: bool, + }, + WithdrawFromNonceAccount { + nonce_account: KeypairEq, + destination_account_pubkey: Pubkey, + lamports: u64, + }, // Program Deployment Deploy(String), // Stake Commands @@ -304,6 +321,14 @@ pub fn parse_command(matches: &ArgMatches<'_>) -> Result parse_show_validators(matches), + // Nonce Commands + ("create-nonce-account", Some(matches)) => parse_nonce_create_account(matches), + ("get-nonce", Some(matches)) => parse_get_nonce(matches), + ("new-nonce", Some(matches)) => parse_new_nonce(matches), + ("show-nonce-account", Some(matches)) => parse_show_nonce_account(matches), + ("withdraw-from-nonce-account", Some(matches)) => { + parse_withdraw_from_nonce_account(matches) + } // Program Deployment ("deploy", Some(matches)) => Ok(CliCommandInfo { command: CliCommand::Deploy(matches.value_of("program_location").unwrap().to_string()), @@ -497,6 +522,7 @@ pub fn parse_command(matches: &ArgMatches<'_>) -> Result { eprintln!("{}", matches.usage()); Err(CliError::CommandNotRecognized( @@ -1035,6 +1061,39 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { process_show_validators(&rpc_client, *use_lamports_unit) } + // Nonce Commands + + // Create nonce account + CliCommand::CreateNonceAccount { + nonce_account, + lamports, + } => process_create_nonce_account(&rpc_client, config, nonce_account, *lamports), + // 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) => { + process_new_nonce(&rpc_client, config, nonce_account) + } + // Show the contents of a nonce account + CliCommand::ShowNonceAccount { + nonce_account_pubkey, + use_lamports_unit, + } => process_show_nonce_account(&rpc_client, &nonce_account_pubkey, *use_lamports_unit), + // Withdraw lamports from a nonce account + CliCommand::WithdrawFromNonceAccount { + nonce_account, + destination_account_pubkey, + lamports, + } => process_withdraw_from_nonce_account( + &rpc_client, + config, + &nonce_account, + &destination_account_pubkey, + *lamports, + ), + // Program Deployment // Deploy a custom program to the chain @@ -1419,6 +1478,7 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' .setting(AppSettings::SubcommandRequiredElseHelp) .subcommand(SubCommand::with_name("address").about("Get your public key")) .cluster_query_subcommands() + .nonce_subcommands() .subcommand( SubCommand::with_name("deploy") .about("Deploy a program") diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 9555271a9d..18d68fada8 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -5,6 +5,7 @@ pub mod cli; pub mod cluster_query; pub mod config; pub mod display; +pub mod nonce; pub mod stake; pub mod storage; pub mod validator_info; diff --git a/cli/src/nonce.rs b/cli/src/nonce.rs new file mode 100644 index 0000000000..fea194aaa1 --- /dev/null +++ b/cli/src/nonce.rs @@ -0,0 +1,506 @@ +use crate::cli::{ + build_balance_message, check_account_for_fee, check_unique_pubkeys, + log_instruction_custom_error, CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult, +}; +use clap::{App, Arg, ArgMatches, SubCommand}; +use solana_clap_utils::{input_parsers::*, input_validators::*}; +use solana_client::rpc_client::RpcClient; +use solana_sdk::{ + account_utils::State, + hash::Hash, + nonce_instruction::{create_nonce_account, nonce, withdraw, NonceError}, + nonce_program, + nonce_state::NonceState, + pubkey::Pubkey, + signature::{Keypair, KeypairUtil}, + system_instruction::SystemError, + transaction::Transaction, +}; + +pub trait NonceSubCommands { + fn nonce_subcommands(self) -> Self; +} + +impl NonceSubCommands for App<'_, '_> { + fn nonce_subcommands(self) -> Self { + self.subcommand( + SubCommand::with_name("create-nonce-account") + .about("Create a nonce account") + .arg( + Arg::with_name("nonce_account_keypair") + .index(1) + .value_name("NONCE ACCOUNT") + .takes_value(true) + .required(true) + .validator(is_keypair_or_ask_keyword) + .help("Keypair of the nonce account to fund"), + ) + .arg( + Arg::with_name("amount") + .index(2) + .value_name("AMOUNT") + .takes_value(true) + .required(true) + .validator(is_amount) + .help("The amount to load the nonce account with (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"), + ), + ) + .subcommand( + SubCommand::with_name("get-nonce") + .about("Get the current nonce value") + .arg( + Arg::with_name("nonce_account_pubkey") + .index(1) + .value_name("NONCE ACCOUNT") + .takes_value(true) + .required(true) + .validator(is_pubkey_or_keypair) + .help("Address of the nonce account to display"), + ), + ) + .subcommand( + SubCommand::with_name("new-nonce") + .about("Generate a new nonce, rendering the existing nonce useless") + .arg( + Arg::with_name("nonce_account_keypair") + .index(1) + .value_name("NONCE ACCOUNT") + .takes_value(true) + .required(true) + .validator(is_pubkey_or_keypair) + .help("Address of the nonce account"), + ), + ) + .subcommand( + SubCommand::with_name("show-nonce-account") + .about("Show the contents of a nonce account") + .arg( + Arg::with_name("nonce_account_pubkey") + .index(1) + .value_name("NONCE ACCOUNT") + .takes_value(true) + .required(true) + .validator(is_pubkey_or_keypair) + .help("Address of the nonce 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("withdraw-from-nonce-account") + .about("Withdraw lamports from the nonce account") + .arg( + Arg::with_name("nonce_account_keypair") + .index(1) + .value_name("NONCE ACCOUNT") + .takes_value(true) + .required(true) + .validator(is_keypair_or_ask_keyword) + .help("Nonce account from to withdraw from"), + ) + .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) + .required(true) + .validator(is_amount) + .help("The amount to withdraw from the nonce 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"), + ), + ) + } +} + +pub fn parse_nonce_create_account(matches: &ArgMatches<'_>) -> Result { + let nonce_account = keypair_of(matches, "nonce_account_keypair").unwrap(); + let lamports = amount_of(matches, "amount", "unit").unwrap(); + + Ok(CliCommandInfo { + command: CliCommand::CreateNonceAccount { + nonce_account: nonce_account.into(), + lamports, + }, + require_keypair: true, + }) +} + +pub fn parse_get_nonce(matches: &ArgMatches<'_>) -> Result { + let nonce_account_pubkey = pubkey_of(matches, "nonce_account_pubkey").unwrap(); + + Ok(CliCommandInfo { + command: CliCommand::GetNonce(nonce_account_pubkey), + require_keypair: false, + }) +} + +pub fn parse_new_nonce(matches: &ArgMatches<'_>) -> Result { + let nonce_account = keypair_of(matches, "nonce_account_keypair").unwrap(); + + Ok(CliCommandInfo { + command: CliCommand::NewNonce(nonce_account.into()), + require_keypair: true, + }) +} + +pub fn parse_show_nonce_account(matches: &ArgMatches<'_>) -> Result { + let nonce_account_pubkey = pubkey_of(matches, "nonce_account_pubkey").unwrap(); + let use_lamports_unit = matches.is_present("lamports"); + + Ok(CliCommandInfo { + command: CliCommand::ShowNonceAccount { + nonce_account_pubkey, + use_lamports_unit, + }, + require_keypair: false, + }) +} + +pub fn parse_withdraw_from_nonce_account( + matches: &ArgMatches<'_>, +) -> Result { + let nonce_account = keypair_of(matches, "nonce_account_keypair").unwrap(); + let destination_account_pubkey = pubkey_of(matches, "destination_account_pubkey").unwrap(); + let lamports = amount_of(matches, "amount", "unit").unwrap(); + + Ok(CliCommandInfo { + command: CliCommand::WithdrawFromNonceAccount { + nonce_account: nonce_account.into(), + destination_account_pubkey, + lamports, + }, + require_keypair: true, + }) +} + +pub fn process_create_nonce_account( + rpc_client: &RpcClient, + config: &CliConfig, + nonce_account: &Keypair, + lamports: u64, +) -> ProcessResult { + let nonce_account_pubkey = nonce_account.pubkey(); + check_unique_pubkeys( + (&config.keypair.pubkey(), "cli keypair".to_string()), + (&nonce_account_pubkey, "nonce_account_pubkey".to_string()), + )?; + + if rpc_client.get_account(&nonce_account_pubkey).is_ok() { + return Err(CliError::BadParameter(format!( + "Unable to create nonce account. Nonce account already exists: {}", + nonce_account_pubkey + )) + .into()); + } + + let minimum_balance = rpc_client.get_minimum_balance_for_rent_exemption(NonceState::size())?; + if lamports < minimum_balance { + return Err(CliError::BadParameter(format!( + "need at least {} lamports for nonce account to be rent exempt, provided lamports: {}", + minimum_balance, lamports + )) + .into()); + } + + let ixs = create_nonce_account(&config.keypair.pubkey(), &nonce_account_pubkey, 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, nonce_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, nonce_account]); + log_instruction_custom_error::(result) +} + +pub fn process_get_nonce(rpc_client: &RpcClient, nonce_account_pubkey: &Pubkey) -> ProcessResult { + let nonce_account = rpc_client.get_account(nonce_account_pubkey)?; + if nonce_account.owner != nonce_program::id() { + return Err(CliError::RpcRequestError( + format!("{:?} is not a nonce account", nonce_account_pubkey).to_string(), + ) + .into()); + } + match nonce_account.state() { + Ok(NonceState::Uninitialized) => Ok("Nonce account is uninitialized".to_string()), + Ok(NonceState::Initialized(_, hash)) => Ok(format!("{:?}", hash)), + Err(err) => Err(CliError::RpcRequestError(format!( + "Account data could not be deserialized to nonce state: {:?}", + err + )) + .into()), + } +} + +pub fn process_new_nonce( + rpc_client: &RpcClient, + config: &CliConfig, + nonce_account: &Keypair, +) -> ProcessResult { + let nonce_account_pubkey = nonce_account.pubkey(); + check_unique_pubkeys( + (&config.keypair.pubkey(), "cli keypair".to_string()), + (&nonce_account_pubkey, "nonce_account_pubkey".to_string()), + )?; + + if rpc_client.get_account(&nonce_account_pubkey).is_err() { + return Err(CliError::BadParameter( + "Unable to create new nonce, no nonce account found".to_string(), + ) + .into()); + } + + let ix = nonce(&nonce_account_pubkey); + let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; + let mut tx = Transaction::new_signed_with_payer( + vec![ix], + Some(&config.keypair.pubkey()), + &[&config.keypair, nonce_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, nonce_account]); + log_instruction_custom_error::(result) +} + +pub fn process_show_nonce_account( + rpc_client: &RpcClient, + nonce_account_pubkey: &Pubkey, + use_lamports_unit: bool, +) -> ProcessResult { + let nonce_account = rpc_client.get_account(nonce_account_pubkey)?; + if nonce_account.owner != nonce_program::id() { + return Err(CliError::RpcRequestError( + format!("{:?} is not a nonce account", nonce_account_pubkey).to_string(), + ) + .into()); + } + let print_account = |hash: Option| { + println!( + "balance: {}", + build_balance_message(nonce_account.lamports, use_lamports_unit, true) + ); + println!( + "minimum balance required: {}", + build_balance_message( + rpc_client.get_minimum_balance_for_rent_exemption(NonceState::size())?, + use_lamports_unit, + true + ) + ); + match hash { + Some(hash) => println!("nonce: {}", hash), + None => println!("nonce: uninitialized"), + } + Ok("".to_string()) + }; + match nonce_account.state() { + Ok(NonceState::Uninitialized) => print_account(None), + Ok(NonceState::Initialized(_, hash)) => print_account(Some(hash)), + Err(err) => Err(CliError::RpcRequestError(format!( + "Account data could not be deserialized to nonce state: {:?}", + err + )) + .into()), + } +} + +pub fn process_withdraw_from_nonce_account( + rpc_client: &RpcClient, + config: &CliConfig, + nonce_account: &Keypair, + destination_account_pubkey: &Pubkey, + lamports: u64, +) -> ProcessResult { + let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; + + let ix = withdraw( + &nonce_account.pubkey(), + destination_account_pubkey, + lamports, + ); + let mut tx = Transaction::new_signed_with_payer( + vec![ix], + Some(&config.keypair.pubkey()), + &[&config.keypair, nonce_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, nonce_account]); + 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) + } + + #[test] + fn test_parse_command() { + let test_commands = app("test", "desc", "version"); + let (keypair_file, mut tmp_file) = make_tmp_file(); + let nonce_account_keypair = Keypair::new(); + write_keypair(&nonce_account_keypair, tmp_file.as_file_mut()).unwrap(); + let nonce_account_pubkey = nonce_account_keypair.pubkey(); + let nonce_account_string = nonce_account_pubkey.to_string(); + + // Test CreateNonceAccount SubCommand + let test_create_nonce_account = test_commands.clone().get_matches_from(vec![ + "test", + "create-nonce-account", + &keypair_file, + "50", + "lamports", + ]); + assert_eq!( + parse_command(&test_create_nonce_account).unwrap(), + CliCommandInfo { + command: CliCommand::CreateNonceAccount { + nonce_account: read_keypair_file(&keypair_file).unwrap().into(), + lamports: 50 + }, + require_keypair: true + } + ); + + // Test GetNonce Subcommand + let test_get_nonce = test_commands.clone().get_matches_from(vec![ + "test", + "get-nonce", + &nonce_account_string, + ]); + assert_eq!( + parse_command(&test_get_nonce).unwrap(), + CliCommandInfo { + command: CliCommand::GetNonce(nonce_account_keypair.pubkey(),), + require_keypair: false + } + ); + + // Test NewNonce SubCommand + let test_new_nonce = + test_commands + .clone() + .get_matches_from(vec!["test", "new-nonce", &keypair_file]); + assert_eq!( + parse_command(&test_new_nonce).unwrap(), + CliCommandInfo { + command: CliCommand::NewNonce(read_keypair_file(&keypair_file).unwrap().into()), + require_keypair: true + } + ); + + // Test ShowNonceAccount Subcommand + let test_show_nonce_account = test_commands.clone().get_matches_from(vec![ + "test", + "show-nonce-account", + &nonce_account_string, + ]); + assert_eq!( + parse_command(&test_show_nonce_account).unwrap(), + CliCommandInfo { + command: CliCommand::ShowNonceAccount { + nonce_account_pubkey: nonce_account_keypair.pubkey(), + use_lamports_unit: false, + }, + require_keypair: false + } + ); + + // Test WithdrawFromNonceAccount Subcommand + let test_withdraw_from_nonce_account = test_commands.clone().get_matches_from(vec![ + "test", + "withdraw-from-nonce-account", + &keypair_file, + &nonce_account_string, + "42", + "lamports", + ]); + assert_eq!( + parse_command(&test_withdraw_from_nonce_account).unwrap(), + CliCommandInfo { + command: CliCommand::WithdrawFromNonceAccount { + nonce_account: read_keypair_file(&keypair_file).unwrap().into(), + destination_account_pubkey: nonce_account_pubkey, + lamports: 42 + }, + require_keypair: true + } + ); + + let test_withdraw_from_nonce_account = test_commands.clone().get_matches_from(vec![ + "test", + "withdraw-from-nonce-account", + &keypair_file, + &nonce_account_string, + "42", + "SOL", + ]); + assert_eq!( + parse_command(&test_withdraw_from_nonce_account).unwrap(), + CliCommandInfo { + command: CliCommand::WithdrawFromNonceAccount { + nonce_account: read_keypair_file(&keypair_file).unwrap().into(), + destination_account_pubkey: nonce_account_pubkey, + lamports: 42000000000 + }, + require_keypair: true + } + ); + } +} diff --git a/cli/tests/nonce.rs b/cli/tests/nonce.rs new file mode 100644 index 0000000000..4840e086b7 --- /dev/null +++ b/cli/tests/nonce.rs @@ -0,0 +1,118 @@ +use solana_cli::cli::{process_command, request_and_confirm_airdrop, CliCommand, CliConfig}; +use solana_client::rpc_client::RpcClient; +use solana_drone::drone::run_local_drone; +use solana_sdk::{ + hash::Hash, + pubkey::Pubkey, + signature::{read_keypair_file, write_keypair, KeypairUtil}, +}; +use std::fs::remove_dir_all; +use std::sync::mpsc::channel; + +#[cfg(test)] +use solana_core::validator::new_validator_for_tests; +use std::thread::sleep; +use std::time::Duration; + +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 check_balance(expected_balance: u64, client: &RpcClient, pubkey: &Pubkey) { + (0..5).for_each(|tries| { + let balance = client.retry_get_balance(pubkey, 1).unwrap().unwrap(); + if balance == expected_balance { + return; + } + if tries == 4 { + assert_eq!(balance, expected_balance); + } + sleep(Duration::from_millis(500)); + }); +} + +#[test] +fn test_nonce() { + let (server, leader_data, alice, ledger_path) = new_validator_for_tests(); + let (sender, receiver) = channel(); + run_local_drone(alice, sender, None); + let drone_addr = receiver.recv().unwrap(); + + let rpc_client = RpcClient::new_socket(leader_data.rpc); + + let mut config_payer = CliConfig::default(); + config_payer.json_rpc_url = + format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); + + let mut config_nonce = CliConfig::default(); + config_nonce.json_rpc_url = + format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); + let (keypair_file, mut tmp_file) = make_tmp_file(); + write_keypair(&config_nonce.keypair, tmp_file.as_file_mut()).unwrap(); + + request_and_confirm_airdrop( + &rpc_client, + &drone_addr, + &config_payer.keypair.pubkey(), + 2000, + ) + .unwrap(); + check_balance(2000, &rpc_client, &config_payer.keypair.pubkey()); + + // Create nonce account + config_payer.command = CliCommand::CreateNonceAccount { + nonce_account: read_keypair_file(&keypair_file).unwrap().into(), + lamports: 1000, + }; + process_command(&config_payer).unwrap(); + check_balance(1000, &rpc_client, &config_payer.keypair.pubkey()); + check_balance(1000, &rpc_client, &config_nonce.keypair.pubkey()); + + // Get nonce + config_payer.command = CliCommand::GetNonce(config_nonce.keypair.pubkey()); + let first_nonce_string = process_command(&config_payer).unwrap(); + let first_nonce = first_nonce_string.parse::().unwrap(); + + // Get nonce + config_payer.command = CliCommand::GetNonce(config_nonce.keypair.pubkey()); + let second_nonce_string = process_command(&config_payer).unwrap(); + let second_nonce = second_nonce_string.parse::().unwrap(); + + assert_eq!(first_nonce, second_nonce); + + // New nonce + config_payer.command = CliCommand::NewNonce(read_keypair_file(&keypair_file).unwrap().into()); + process_command(&config_payer).unwrap(); + + // Get nonce + config_payer.command = CliCommand::GetNonce(config_nonce.keypair.pubkey()); + let third_nonce_string = process_command(&config_payer).unwrap(); + let third_nonce = third_nonce_string.parse::().unwrap(); + + assert_ne!(first_nonce, third_nonce); + + // Withdraw from nonce account + let payee_pubkey = Pubkey::new_rand(); + config_payer.command = CliCommand::WithdrawFromNonceAccount { + nonce_account: read_keypair_file(&keypair_file).unwrap().into(), + destination_account_pubkey: payee_pubkey, + lamports: 100, + }; + process_command(&config_payer).unwrap(); + check_balance(1000, &rpc_client, &config_payer.keypair.pubkey()); + check_balance(900, &rpc_client, &config_nonce.keypair.pubkey()); + check_balance(100, &rpc_client, &payee_pubkey); + + // Show nonce account + config_payer.command = CliCommand::ShowNonceAccount { + nonce_account_pubkey: config_nonce.keypair.pubkey(), + use_lamports_unit: true, + }; + process_command(&config_payer).unwrap(); + + server.close().unwrap(); + remove_dir_all(ledger_path).unwrap(); +}