From ed53a70b5c59f4b939af55c02698eb7d6823a97a Mon Sep 17 00:00:00 2001 From: Tyera Eulberg Date: Thu, 14 May 2020 12:24:14 -0600 Subject: [PATCH] Cli: transfer ALL; check spend+fee in client (#10012) * lamports->SOL in user-facing error msg * Check for sufficient balance for spend and fee * Add ALL option to solana transfer * Rework TransferAmount to check for sign_only in parse * Refactor TransferAmount & fee-check handling to be more general * Add addl checks mechanism * Move checks out of cli.rs * Rename to SpendAmount to be more general & move * Impl ALL/spend helpers for create-nonce-account * Impl spend helpers for create-vote-account * Impl ALL/spend helpers for create-stake-account * Impl spend helpers for ping * Impl ALL/spend helpers for pay * Impl spend helpers for validator-info * Remove unused fns * Remove retry_get_balance * Add a couple unit tests * Rework send_util fn signatures --- clap-utils/src/input_validators.rs | 11 + cli/src/checks.rs | 216 +++++++++++++++++ cli/src/cli.rs | 327 +++++++++++++------------- cli/src/cluster_query.rs | 23 +- cli/src/lib.rs | 2 + cli/src/nonce.rs | 130 +++++----- cli/src/spend_utils.rs | 158 +++++++++++++ cli/src/stake.rs | 133 ++++++----- cli/src/storage.rs | 10 +- cli/src/test_utils.rs | 2 +- cli/src/validator_info.rs | 96 ++++---- cli/src/vote.rs | 83 ++++--- cli/tests/nonce.rs | 9 +- cli/tests/pay.rs | 15 +- cli/tests/request_airdrop.rs | 3 +- cli/tests/stake.rs | 37 +-- cli/tests/transfer.rs | 91 ++++++- client/src/mock_rpc_client_request.rs | 2 +- client/src/rpc_client.rs | 18 -- install/src/command.rs | 4 +- sdk/src/system_instruction.rs | 2 +- 21 files changed, 929 insertions(+), 443 deletions(-) create mode 100644 cli/src/checks.rs create mode 100644 cli/src/spend_utils.rs diff --git a/clap-utils/src/input_validators.rs b/clap-utils/src/input_validators.rs index 73a2a6a10..fc4fbc693 100644 --- a/clap-utils/src/input_validators.rs +++ b/clap-utils/src/input_validators.rs @@ -147,6 +147,17 @@ pub fn is_amount(amount: String) -> Result<(), String> { } } +pub fn is_amount_or_all(amount: String) -> Result<(), String> { + if amount.parse::().is_ok() || amount.parse::().is_ok() || amount == "ALL" { + Ok(()) + } else { + Err(format!( + "Unable to parse input amount as integer or float, provided: {}", + amount + )) + } +} + pub fn is_rfc3339_datetime(value: String) -> Result<(), String> { DateTime::parse_from_rfc3339(&value) .map(|_| ()) diff --git a/cli/src/checks.rs b/cli/src/checks.rs new file mode 100644 index 000000000..8b6f5ec65 --- /dev/null +++ b/cli/src/checks.rs @@ -0,0 +1,216 @@ +use crate::cli::CliError; +use solana_client::{ + client_error::{ClientError, Result as ClientResult}, + rpc_client::RpcClient, +}; +use solana_sdk::{ + fee_calculator::FeeCalculator, message::Message, native_token::lamports_to_sol, pubkey::Pubkey, +}; + +pub fn check_account_for_fee( + rpc_client: &RpcClient, + account_pubkey: &Pubkey, + fee_calculator: &FeeCalculator, + message: &Message, +) -> Result<(), CliError> { + check_account_for_multiple_fees(rpc_client, account_pubkey, fee_calculator, &[message]) +} + +pub fn check_account_for_multiple_fees( + rpc_client: &RpcClient, + account_pubkey: &Pubkey, + fee_calculator: &FeeCalculator, + messages: &[&Message], +) -> Result<(), CliError> { + let fee = calculate_fee(fee_calculator, messages); + if !check_account_for_balance(rpc_client, account_pubkey, fee) + .map_err(Into::::into)? + { + return Err(CliError::InsufficientFundsForFee(lamports_to_sol(fee))); + } + Ok(()) +} + +pub fn calculate_fee(fee_calculator: &FeeCalculator, messages: &[&Message]) -> u64 { + messages + .iter() + .map(|message| fee_calculator.calculate_fee(message)) + .sum() +} + +pub fn check_account_for_balance( + rpc_client: &RpcClient, + account_pubkey: &Pubkey, + balance: u64, +) -> ClientResult { + let lamports = rpc_client.get_balance(account_pubkey)?; + if lamports != 0 && lamports >= balance { + return Ok(true); + } + Ok(false) +} + +pub fn check_unique_pubkeys( + pubkey0: (&Pubkey, String), + pubkey1: (&Pubkey, String), +) -> Result<(), CliError> { + if pubkey0.0 == pubkey1.0 { + Err(CliError::BadParameter(format!( + "Identical pubkeys found: `{}` and `{}` must be unique", + pubkey0.1, pubkey1.1 + ))) + } else { + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + use solana_client::{ + rpc_request::RpcRequest, + rpc_response::{Response, RpcResponseContext}, + }; + use solana_sdk::system_instruction; + use std::collections::HashMap; + + #[test] + fn test_check_account_for_fees() { + let account_balance = 1; + let account_balance_response = json!(Response { + context: RpcResponseContext { slot: 1 }, + value: json!(account_balance), + }); + let pubkey = Pubkey::new_rand(); + let fee_calculator = FeeCalculator::new(1); + + let pubkey0 = Pubkey::new(&[0; 32]); + let pubkey1 = Pubkey::new(&[1; 32]); + let ix0 = system_instruction::transfer(&pubkey0, &pubkey1, 1); + let message0 = Message::new(&[ix0]); + + let ix0 = system_instruction::transfer(&pubkey0, &pubkey1, 1); + let ix1 = system_instruction::transfer(&pubkey1, &pubkey0, 1); + let message1 = Message::new(&[ix0, ix1]); + + let mut mocks = HashMap::new(); + mocks.insert(RpcRequest::GetBalance, account_balance_response.clone()); + let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks); + assert_eq!( + check_account_for_fee(&rpc_client, &pubkey, &fee_calculator, &message0).unwrap(), + () + ); + + let mut mocks = HashMap::new(); + mocks.insert(RpcRequest::GetBalance, account_balance_response.clone()); + let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks); + assert!(check_account_for_fee(&rpc_client, &pubkey, &fee_calculator, &message1).is_err()); + + let mut mocks = HashMap::new(); + mocks.insert(RpcRequest::GetBalance, account_balance_response); + let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks); + assert!(check_account_for_multiple_fees( + &rpc_client, + &pubkey, + &fee_calculator, + &[&message0, &message0] + ) + .is_err()); + + let account_balance = 2; + let account_balance_response = json!(Response { + context: RpcResponseContext { slot: 1 }, + value: json!(account_balance), + }); + + let mut mocks = HashMap::new(); + mocks.insert(RpcRequest::GetBalance, account_balance_response); + let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks); + + assert_eq!( + check_account_for_multiple_fees( + &rpc_client, + &pubkey, + &fee_calculator, + &[&message0, &message0] + ) + .unwrap(), + () + ); + } + + #[test] + fn test_check_account_for_balance() { + let account_balance = 50; + let account_balance_response = json!(Response { + context: RpcResponseContext { slot: 1 }, + value: json!(account_balance), + }); + let pubkey = Pubkey::new_rand(); + + let mut mocks = HashMap::new(); + mocks.insert(RpcRequest::GetBalance, account_balance_response); + let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks); + + assert_eq!( + check_account_for_balance(&rpc_client, &pubkey, 1).unwrap(), + true + ); + assert_eq!( + check_account_for_balance(&rpc_client, &pubkey, account_balance).unwrap(), + true + ); + assert_eq!( + check_account_for_balance(&rpc_client, &pubkey, account_balance + 1).unwrap(), + false + ); + } + + #[test] + fn test_calculate_fee() { + let fee_calculator = FeeCalculator::new(1); + // No messages, no fee. + assert_eq!(calculate_fee(&fee_calculator, &[]), 0); + + // No signatures, no fee. + let message = Message::new(&[]); + assert_eq!(calculate_fee(&fee_calculator, &[&message, &message]), 0); + + // One message w/ one signature, a fee. + let pubkey0 = Pubkey::new(&[0; 32]); + let pubkey1 = Pubkey::new(&[1; 32]); + let ix0 = system_instruction::transfer(&pubkey0, &pubkey1, 1); + let message0 = Message::new(&[ix0]); + assert_eq!(calculate_fee(&fee_calculator, &[&message0]), 1); + + // Two messages, additive fees. + let ix0 = system_instruction::transfer(&pubkey0, &pubkey1, 1); + let ix1 = system_instruction::transfer(&pubkey1, &pubkey0, 1); + let message1 = Message::new(&[ix0, ix1]); + assert_eq!(calculate_fee(&fee_calculator, &[&message0, &message1]), 3); + } + + #[test] + fn test_check_unique_pubkeys() { + let pubkey0 = Pubkey::new_rand(); + let pubkey_clone = pubkey0.clone(); + let pubkey1 = Pubkey::new_rand(); + + assert_eq!( + check_unique_pubkeys((&pubkey0, "foo".to_string()), (&pubkey1, "bar".to_string())) + .unwrap(), + () + ); + assert_eq!( + check_unique_pubkeys((&pubkey0, "foo".to_string()), (&pubkey1, "foo".to_string())) + .unwrap(), + () + ); + assert!(check_unique_pubkeys( + (&pubkey0, "foo".to_string()), + (&pubkey_clone, "bar".to_string()) + ) + .is_err()); + } +} diff --git a/cli/src/cli.rs b/cli/src/cli.rs index bb364759b..77ff920ea 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -1,9 +1,11 @@ use crate::{ + checks::*, cli_output::{CliAccount, CliSignOnlyData, CliSignature, OutputFormat}, cluster_query::*, display::println_name_value, nonce::{self, *}, offline::{blockhash_query::BlockhashQuery, *}, + spend_utils::*, stake::*, storage::*, validator_info::*, @@ -24,7 +26,7 @@ use solana_clap_utils::{ ArgConstant, }; use solana_client::{ - client_error::{ClientErrorKind, Result as ClientResult}, + client_error::{ClientError, ClientErrorKind, Result as ClientResult}, rpc_client::RpcClient, rpc_config::RpcLargestAccountsFilter, rpc_response::{RpcAccount, RpcKeyedAccount}, @@ -163,7 +165,7 @@ pub fn nonce_authority_arg<'a, 'b>() -> Arg<'a, 'b> { #[derive(Default, Debug, PartialEq)] pub struct PayCommand { - pub lamports: u64, + pub amount: SpendAmount, pub to: Pubkey, pub timestamp: Option>, pub timestamp_pubkey: Option, @@ -257,7 +259,7 @@ pub enum CliCommand { nonce_account: SignerIndex, seed: Option, nonce_authority: Option, - lamports: u64, + amount: SpendAmount, }, GetNonce(Pubkey), NewNonce { @@ -283,7 +285,7 @@ pub enum CliCommand { staker: Option, withdrawer: Option, lockup: Lockup, - lamports: u64, + amount: SpendAmount, sign_only: bool, blockhash_query: BlockhashQuery, nonce_account: Option, @@ -432,7 +434,7 @@ pub enum CliCommand { }, TimeElapsed(Pubkey, Pubkey, DateTime), // TimeElapsed(to, process_id, timestamp) Transfer { - lamports: u64, + amount: SpendAmount, to: Pubkey, from: SignerIndex, sign_only: bool, @@ -451,14 +453,20 @@ pub struct CliCommandInfo { pub signers: CliSigners, } -#[derive(Debug, Error, PartialEq)] +#[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("insuficient funds for fee")] - InsufficientFundsForFee, + #[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}")] @@ -852,7 +860,7 @@ pub fn parse_command( } } ("pay", Some(matches)) => { - let lamports = lamports_of_sol(matches, "amount").unwrap(); + 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 @@ -888,7 +896,7 @@ pub fn parse_command( Ok(CliCommandInfo { command: CliCommand::Pay(PayCommand { - lamports, + amount, to, timestamp, timestamp_pubkey, @@ -958,7 +966,7 @@ pub fn parse_command( }) } ("transfer", Some(matches)) => { - let lamports = lamports_of_sol(matches, "amount").unwrap(); + 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"); @@ -984,7 +992,7 @@ pub fn parse_command( Ok(CliCommandInfo { command: CliCommand::Transfer { - lamports, + amount, to, sign_only, no_wait, @@ -1011,48 +1019,6 @@ pub fn parse_command( pub type ProcessResult = Result>; -pub fn check_account_for_fee( - rpc_client: &RpcClient, - account_pubkey: &Pubkey, - fee_calculator: &FeeCalculator, - message: &Message, -) -> Result<(), Box> { - check_account_for_multiple_fees(rpc_client, account_pubkey, fee_calculator, &[message]) -} - -fn check_account_for_multiple_fees( - rpc_client: &RpcClient, - account_pubkey: &Pubkey, - fee_calculator: &FeeCalculator, - messages: &[&Message], -) -> Result<(), Box> { - let balance = rpc_client.retry_get_balance(account_pubkey, 5)?; - if let Some(lamports) = balance { - let fee = messages - .iter() - .map(|message| fee_calculator.calculate_fee(message)) - .sum(); - if lamports != 0 && lamports >= fee { - return Ok(()); - } - } - Err(CliError::InsufficientFundsForFee.into()) -} - -pub fn check_unique_pubkeys( - pubkey0: (&Pubkey, String), - pubkey1: (&Pubkey, String), -) -> Result<(), CliError> { - if pubkey0.0 == pubkey1.0 { - Err(CliError::BadParameter(format!( - "Identical pubkeys found: `{}` and `{}` must be unique", - pubkey0.1, pubkey1.1 - ))) - } else { - Ok(()) - } -} - pub fn get_blockhash_and_fee_calculator( rpc_client: &RpcClient, sign_only: bool, @@ -1173,21 +1139,10 @@ fn process_airdrop( build_balance_message(lamports, false, true), faucet_addr ); - let previous_balance = match rpc_client.retry_get_balance(&pubkey, 5)? { - Some(lamports) => lamports, - None => { - return Err(CliError::RpcRequestError( - "Received result of an unexpected type".to_string(), - ) - .into()) - } - }; request_and_confirm_airdrop(&rpc_client, faucet_addr, &pubkey, lamports, &config)?; - let current_balance = rpc_client - .retry_get_balance(&pubkey, 5)? - .unwrap_or(previous_balance); + let current_balance = rpc_client.get_balance(&pubkey)?; Ok(build_balance_message(current_balance, false, true)) } @@ -1389,7 +1344,7 @@ fn process_deploy( fn process_pay( rpc_client: &RpcClient, config: &CliConfig, - lamports: u64, + amount: SpendAmount, to: &Pubkey, timestamp: Option>, timestamp_pubkey: Option, @@ -1416,29 +1371,35 @@ fn process_pay( if timestamp == None && *witnesses == None { let nonce_authority = config.signers[nonce_authority]; - let ix = system_instruction::transfer(&config.signers[0].pubkey(), to, lamports); - let message = if let Some(nonce_account) = &nonce_account { - Message::new_with_nonce(vec![ix], None, nonce_account, &nonce_authority.pubkey()) - } else { - Message::new(&[ix]) + 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 { - tx.try_sign(&config.signers, 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(), &blockhash)?; } - check_account_for_fee( - rpc_client, - &config.signers[0].pubkey(), - &fee_calculator, - &tx.message, - )?; + + tx.try_sign(&config.signers, blockhash)?; let result = rpc_client.send_and_confirm_transaction_with_spinner(&tx); log_instruction_custom_error::(result, &config) } @@ -1451,29 +1412,33 @@ fn process_pay( let contract_state = Keypair::new(); - // Initializing contract - let ixs = budget_instruction::on_date( + 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(), - to, - &contract_state.pubkey(), - dt, - &dt_pubkey, - cancelable, - lamports, - ); - let message = Message::new(&ixs); + 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)?; - 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); let signature = log_instruction_custom_error::(result, &config)?; Ok(json!({ @@ -1494,16 +1459,26 @@ fn process_pay( let contract_state = Keypair::new(); - // Initializing contract - let ixs = budget_instruction::when_signed( + 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(), - to, - &contract_state.pubkey(), - &witness, - cancelable, - lamports, - ); - let message = Message::new(&ixs); + build_message, + )?; let mut tx = Transaction::new_unsigned(message); if sign_only { tx.try_partial_sign(&[config.signers[0], &contract_state], blockhash)?; @@ -1511,12 +1486,6 @@ fn process_pay( } else { tx.try_sign(&[config.signers[0], &contract_state], blockhash)?; let result = rpc_client.send_and_confirm_transaction_with_spinner(&tx); - check_account_for_fee( - rpc_client, - &config.signers[0].pubkey(), - &fee_calculator, - &tx.message, - )?; let signature = log_instruction_custom_error::(result, &config)?; Ok(json!({ "signature": signature, @@ -1576,7 +1545,7 @@ fn process_time_elapsed( fn process_transfer( rpc_client: &RpcClient, config: &CliConfig, - lamports: u64, + amount: SpendAmount, to: &Pubkey, from: SignerIndex, sign_only: bool, @@ -1595,38 +1564,50 @@ fn process_transfer( let (recent_blockhash, fee_calculator) = blockhash_query.get_blockhash_and_fee_calculator(rpc_client)?; - let ixs = vec![system_instruction::transfer(&from.pubkey(), to, lamports)]; let nonce_authority = config.signers[nonce_authority]; let fee_payer = config.signers[fee_payer]; - let message = 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 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)?; } - check_account_for_fee( - rpc_client, - &tx.message.account_keys[0], - &fee_calculator, - &tx.message, - )?; let result = if no_wait { rpc_client.send_transaction(&tx) } else { @@ -1786,14 +1767,14 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { nonce_account, seed, nonce_authority, - lamports, + amount, } => process_create_nonce_account( &rpc_client, config, *nonce_account, seed.clone(), *nonce_authority, - *lamports, + *amount, ), // Get the current nonce CliCommand::GetNonce(nonce_account_pubkey) => { @@ -1845,7 +1826,7 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { staker, withdrawer, lockup, - lamports, + amount, sign_only, blockhash_query, ref nonce_account, @@ -1860,7 +1841,7 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { staker, withdrawer, lockup, - *lamports, + *amount, *sign_only, blockhash_query, nonce_account.as_ref(), @@ -2166,7 +2147,7 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { CliCommand::DecodeTransaction(transaction) => process_decode_transaction(transaction), // If client has positive balance, pay lamports to another address CliCommand::Pay(PayCommand { - lamports, + amount, to, timestamp, timestamp_pubkey, @@ -2179,7 +2160,7 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { }) => process_pay( &rpc_client, config, - *lamports, + *amount, &to, *timestamp, *timestamp_pubkey, @@ -2213,7 +2194,7 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { process_time_elapsed(&rpc_client, config, &to, &pubkey, *dt) } CliCommand::Transfer { - lamports, + amount, to, from, sign_only, @@ -2225,7 +2206,7 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { } => process_transfer( &rpc_client, config, - *lamports, + *amount, to, *from, *sign_only, @@ -2520,9 +2501,9 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' .index(2) .value_name("AMOUNT") .takes_value(true) - .validator(is_amount) + .validator(is_amount_or_all) .required(true) - .help("The amount to send, in SOL"), + .help("The amount to send, in SOL; accepts keyword ALL"), ) .arg( Arg::with_name("timestamp") @@ -2632,9 +2613,9 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' .index(2) .value_name("AMOUNT") .takes_value(true) - .validator(is_amount) + .validator(is_amount_or_all) .required(true) - .help("The amount to send, in SOL"), + .help("The amount to send, in SOL; accepts keyword ALL"), ) .arg( pubkey!(Arg::with_name("from") @@ -2986,7 +2967,7 @@ mod tests { parse_command(&test_pay, &keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Pay(PayCommand { - lamports: 50_000_000_000, + amount: SpendAmount::Some(50_000_000_000), to: pubkey, ..PayCommand::default() }), @@ -3009,7 +2990,7 @@ mod tests { parse_command(&test_pay_multiple_witnesses, &keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Pay(PayCommand { - lamports: 50_000_000_000, + amount: SpendAmount::Some(50_000_000_000), to: pubkey, witnesses: Some(vec![witness0, witness1]), ..PayCommand::default() @@ -3029,7 +3010,7 @@ mod tests { parse_command(&test_pay_single_witness, &keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Pay(PayCommand { - lamports: 50_000_000_000, + amount: SpendAmount::Some(50_000_000_000), to: pubkey, witnesses: Some(vec![witness0]), ..PayCommand::default() @@ -3053,7 +3034,7 @@ mod tests { parse_command(&test_pay_timestamp, &keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Pay(PayCommand { - lamports: 50_000_000_000, + amount: SpendAmount::Some(50_000_000_000), to: pubkey, timestamp: Some(dt), timestamp_pubkey: Some(witness0), @@ -3079,7 +3060,7 @@ mod tests { parse_command(&test_pay, &keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Pay(PayCommand { - lamports: 50_000_000_000, + amount: SpendAmount::Some(50_000_000_000), to: pubkey, blockhash_query: BlockhashQuery::None(blockhash), sign_only: true, @@ -3102,7 +3083,7 @@ mod tests { parse_command(&test_pay, &keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Pay(PayCommand { - lamports: 50_000_000_000, + amount: SpendAmount::Some(50_000_000_000), to: pubkey, blockhash_query: BlockhashQuery::FeeCalculator( blockhash_query::Source::Cluster, @@ -3131,7 +3112,7 @@ mod tests { parse_command(&test_pay, &keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Pay(PayCommand { - lamports: 50_000_000_000, + amount: SpendAmount::Some(50_000_000_000), to: pubkey, blockhash_query: BlockhashQuery::FeeCalculator( blockhash_query::Source::NonceAccount(pubkey), @@ -3164,7 +3145,7 @@ mod tests { parse_command(&test_pay, &keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Pay(PayCommand { - lamports: 50_000_000_000, + amount: SpendAmount::Some(50_000_000_000), to: pubkey, blockhash_query: BlockhashQuery::FeeCalculator( blockhash_query::Source::NonceAccount(pubkey), @@ -3202,7 +3183,7 @@ mod tests { parse_command(&test_pay, &keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Pay(PayCommand { - lamports: 50_000_000_000, + amount: SpendAmount::Some(50_000_000_000), to: pubkey, blockhash_query: BlockhashQuery::FeeCalculator( blockhash_query::Source::NonceAccount(pubkey), @@ -3271,7 +3252,7 @@ mod tests { parse_command(&test_pay_multiple_witnesses, &keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Pay(PayCommand { - lamports: 50_000_000_000, + amount: SpendAmount::Some(50_000_000_000), to: pubkey, timestamp: Some(dt), timestamp_pubkey: Some(witness0), @@ -3389,7 +3370,7 @@ mod tests { unix_timestamp: 0, custodian, }, - lamports: 1234, + amount: SpendAmount::Some(30), sign_only: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), nonce_account: None, @@ -3443,7 +3424,7 @@ mod tests { nonce_authority: 0, split_stake_account: 1, seed: None, - lamports: 1234, + lamports: 30, fee_payer: 0, }; config.signers = vec![&keypair, &split_stake_account]; @@ -3462,7 +3443,7 @@ mod tests { config.signers = vec![&keypair]; config.command = CliCommand::Pay(PayCommand { - lamports: 10, + amount: SpendAmount::Some(10), to: bob_pubkey, ..PayCommand::default() }); @@ -3472,7 +3453,7 @@ mod tests { let date_string = "\"2018-09-19T17:30:59Z\""; let dt: DateTime = serde_json::from_str(&date_string).unwrap(); config.command = CliCommand::Pay(PayCommand { - lamports: 10, + amount: SpendAmount::Some(10), to: bob_pubkey, timestamp: Some(dt), timestamp_pubkey: Some(config.signers[0].pubkey()), @@ -3483,7 +3464,7 @@ mod tests { let witness = Pubkey::new_rand(); config.command = CliCommand::Pay(PayCommand { - lamports: 10, + amount: SpendAmount::Some(10), to: bob_pubkey, witnesses: Some(vec![witness]), cancelable: true, @@ -3605,14 +3586,14 @@ mod tests { assert!(process_command(&config).is_err()); config.command = CliCommand::Pay(PayCommand { - lamports: 10, + amount: SpendAmount::Some(10), to: bob_pubkey, ..PayCommand::default() }); assert!(process_command(&config).is_err()); config.command = CliCommand::Pay(PayCommand { - lamports: 10, + amount: SpendAmount::Some(10), to: bob_pubkey, timestamp: Some(dt), timestamp_pubkey: Some(config.signers[0].pubkey()), @@ -3621,7 +3602,7 @@ mod tests { assert!(process_command(&config).is_err()); config.command = CliCommand::Pay(PayCommand { - lamports: 10, + amount: SpendAmount::Some(10), to: bob_pubkey, witnesses: Some(vec![witness]), cancelable: true, @@ -3688,7 +3669,29 @@ mod tests { parse_command(&test_transfer, &default_keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Transfer { - lamports: 42_000_000_000, + 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, @@ -3714,7 +3717,7 @@ mod tests { parse_command(&test_transfer, &default_keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Transfer { - lamports: 42_000_000_000, + amount: SpendAmount::Some(42_000_000_000), to: to_pubkey, from: 0, sign_only: false, @@ -3744,7 +3747,7 @@ mod tests { parse_command(&test_transfer, &default_keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Transfer { - lamports: 42_000_000_000, + amount: SpendAmount::Some(42_000_000_000), to: to_pubkey, from: 0, sign_only: true, @@ -3779,7 +3782,7 @@ mod tests { parse_command(&test_transfer, &default_keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Transfer { - lamports: 42_000_000_000, + amount: SpendAmount::Some(42_000_000_000), to: to_pubkey, from: 0, sign_only: false, @@ -3818,7 +3821,7 @@ mod tests { parse_command(&test_transfer, &default_keypair_file, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Transfer { - lamports: 42_000_000_000, + amount: SpendAmount::Some(42_000_000_000), to: to_pubkey, from: 0, sign_only: false, diff --git a/cli/src/cluster_query.rs b/cli/src/cluster_query.rs index f649482e2..f35d0a9a0 100644 --- a/cli/src/cluster_query.rs +++ b/cli/src/cluster_query.rs @@ -1,7 +1,8 @@ use crate::{ - cli::{check_account_for_fee, CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult}, + cli::{CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult}, cli_output::*, display::println_name_value, + spend_utils::{resolve_spend_tx_and_check_account_balance, SpendAmount}, }; use clap::{value_t, value_t_or_exit, App, AppSettings, Arg, ArgMatches, SubCommand}; use console::{style, Emoji}; @@ -926,18 +927,22 @@ pub fn process_ping( let (recent_blockhash, fee_calculator) = rpc_client.get_new_blockhash(&last_blockhash)?; last_blockhash = recent_blockhash; - let ix = system_instruction::transfer(&config.signers[0].pubkey(), &to, lamports); - let message = Message::new(&[ix]); - let mut transaction = Transaction::new_unsigned(message); - transaction.try_sign(&config.signers, recent_blockhash)?; - check_account_for_fee( + let build_message = |lamports| { + let ix = system_instruction::transfer(&config.signers[0].pubkey(), &to, lamports); + Message::new(&[ix]) + }; + let (message, _) = resolve_spend_tx_and_check_account_balance( rpc_client, - &config.signers[0].pubkey(), + false, + SpendAmount::Some(lamports), &fee_calculator, - &transaction.message, + &config.signers[0].pubkey(), + build_message, )?; + let mut tx = Transaction::new_unsigned(message); + tx.try_sign(&config.signers, recent_blockhash)?; - match rpc_client.send_transaction(&transaction) { + match rpc_client.send_transaction(&tx) { Ok(signature) => { let transaction_sent = Instant::now(); loop { diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 9b8a94eff..7c4c4049d 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -21,12 +21,14 @@ macro_rules! pubkey { #[macro_use] extern crate serde_derive; +pub mod checks; pub mod cli; pub mod cli_output; pub mod cluster_query; pub mod display; pub mod nonce; pub mod offline; +pub mod spend_utils; pub mod stake; pub mod storage; pub mod test_utils; diff --git a/cli/src/nonce.rs b/cli/src/nonce.rs index ea6675312..8a84e47a5 100644 --- a/cli/src/nonce.rs +++ b/cli/src/nonce.rs @@ -1,10 +1,11 @@ use crate::{ + checks::{check_account_for_fee, check_unique_pubkeys}, cli::{ - check_account_for_fee, check_unique_pubkeys, generate_unique_signers, - log_instruction_custom_error, CliCommand, CliCommandInfo, CliConfig, CliError, - ProcessResult, SignerIndex, + generate_unique_signers, log_instruction_custom_error, CliCommand, CliCommandInfo, + CliConfig, CliError, ProcessResult, SignerIndex, }, cli_output::CliNonceAccount, + spend_utils::{resolve_spend_tx_and_check_account_balance, SpendAmount}, }; use clap::{App, Arg, ArgMatches, SubCommand}; use solana_clap_utils::{ @@ -128,8 +129,8 @@ impl NonceSubCommands for App<'_, '_> { .value_name("AMOUNT") .takes_value(true) .required(true) - .validator(is_amount) - .help("The amount to load the nonce account with, in SOL"), + .validator(is_amount_or_all) + .help("The amount to load the nonce account with, in SOL; accepts keyword ALL"), ) .arg( pubkey!(Arg::with_name(NONCE_AUTHORITY_ARG.name) @@ -296,7 +297,7 @@ pub fn parse_nonce_create_account( let (nonce_account, nonce_account_pubkey) = signer_of(matches, "nonce_account_keypair", wallet_manager)?; let seed = matches.value_of("seed").map(|s| s.to_string()); - let lamports = lamports_of_sol(matches, "amount").unwrap(); + let amount = SpendAmount::new_from_matches(matches, "amount"); let nonce_authority = pubkey_of_signer(matches, NONCE_AUTHORITY_ARG.name, wallet_manager)?; let payer_provided = None; @@ -312,7 +313,7 @@ pub fn parse_nonce_create_account( nonce_account: signer_info.index_of(nonce_account_pubkey).unwrap(), seed, nonce_authority, - lamports, + amount, }, signers: signer_info.signers, }) @@ -456,7 +457,7 @@ pub fn process_create_nonce_account( nonce_account: SignerIndex, seed: Option, nonce_authority: Option, - lamports: u64, + amount: SpendAmount, ) -> ProcessResult { let nonce_account_pubkey = config.signers[nonce_account].pubkey(); let nonce_account_address = if let Some(ref seed) = seed { @@ -470,6 +471,40 @@ pub fn process_create_nonce_account( (&nonce_account_address, "nonce_account".to_string()), )?; + let nonce_authority = nonce_authority.unwrap_or_else(|| config.signers[0].pubkey()); + + let build_message = |lamports| { + let ixs = if let Some(seed) = seed.clone() { + create_nonce_account_with_seed( + &config.signers[0].pubkey(), // from + &nonce_account_address, // to + &nonce_account_pubkey, // base + &seed, // seed + &nonce_authority, + lamports, + ) + } else { + create_nonce_account( + &config.signers[0].pubkey(), + &nonce_account_pubkey, + &nonce_authority, + lamports, + ) + }; + Message::new_with_payer(&ixs, Some(&config.signers[0].pubkey())) + }; + + let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; + + let (message, lamports) = resolve_spend_tx_and_check_account_balance( + rpc_client, + false, + amount, + &fee_calculator, + &config.signers[0].pubkey(), + build_message, + )?; + if let Ok(nonce_account) = get_account(rpc_client, &nonce_account_address) { let err_msg = if state_from_account(&nonce_account).is_ok() { format!("Nonce account {} already exists", nonce_account_address) @@ -491,38 +526,8 @@ pub fn process_create_nonce_account( .into()); } - let nonce_authority = nonce_authority.unwrap_or_else(|| config.signers[0].pubkey()); - - let ixs = if let Some(seed) = seed { - create_nonce_account_with_seed( - &config.signers[0].pubkey(), // from - &nonce_account_address, // to - &nonce_account_pubkey, // base - &seed, // seed - &nonce_authority, - lamports, - ) - } else { - create_nonce_account( - &config.signers[0].pubkey(), - &nonce_account_pubkey, - &nonce_authority, - lamports, - ) - }; - - let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; - - let message = Message::new_with_payer(&ixs, Some(&config.signers[0].pubkey())); let mut tx = Transaction::new_unsigned(message); tx.try_sign(&config.signers, recent_blockhash)?; - - check_account_for_fee( - rpc_client, - &config.signers[0].pubkey(), - &fee_calculator, - &tx.message, - )?; let result = rpc_client.send_and_confirm_transaction_with_spinner(&tx); log_instruction_custom_error::(result, &config) } @@ -729,7 +734,7 @@ mod tests { nonce_account: 1, seed: None, nonce_authority: None, - lamports: 50_000_000_000, + amount: SpendAmount::Some(50_000_000_000), }, signers: vec![ read_keypair_file(&default_keypair_file).unwrap().into(), @@ -754,7 +759,7 @@ mod tests { nonce_account: 1, seed: None, nonce_authority: Some(nonce_authority_keypair.pubkey()), - lamports: 50_000_000_000, + amount: SpendAmount::Some(50_000_000_000), }, signers: vec![ read_keypair_file(&default_keypair_file).unwrap().into(), @@ -905,16 +910,18 @@ mod tests { assert!(check_nonce_account(&valid.unwrap(), &nonce_pubkey, &blockhash).is_ok()); let invalid_owner = Account::new_data(1, &data, &Pubkey::new(&[1u8; 32])); - assert_eq!( - check_nonce_account(&invalid_owner.unwrap(), &nonce_pubkey, &blockhash), - Err(CliNonceError::InvalidAccountOwner.into()), - ); + if let CliError::InvalidNonce(err) = + check_nonce_account(&invalid_owner.unwrap(), &nonce_pubkey, &blockhash).unwrap_err() + { + assert_eq!(err, CliNonceError::InvalidAccountOwner,); + } let invalid_data = Account::new_data(1, &"invalid", &system_program::ID); - assert_eq!( - check_nonce_account(&invalid_data.unwrap(), &nonce_pubkey, &blockhash), - Err(CliNonceError::InvalidAccountData.into()), - ); + if let CliError::InvalidNonce(err) = + check_nonce_account(&invalid_data.unwrap(), &nonce_pubkey, &blockhash).unwrap_err() + { + assert_eq!(err, CliNonceError::InvalidAccountData,); + } let data = Versions::new_current(State::Initialized(nonce::state::Data { authority: nonce_pubkey, @@ -922,10 +929,11 @@ mod tests { fee_calculator: FeeCalculator::default(), })); let invalid_hash = Account::new_data(1, &data, &system_program::ID); - assert_eq!( - check_nonce_account(&invalid_hash.unwrap(), &nonce_pubkey, &blockhash), - Err(CliNonceError::InvalidHash.into()), - ); + if let CliError::InvalidNonce(err) = + check_nonce_account(&invalid_hash.unwrap(), &nonce_pubkey, &blockhash).unwrap_err() + { + assert_eq!(err, CliNonceError::InvalidHash,); + } let data = Versions::new_current(State::Initialized(nonce::state::Data { authority: Pubkey::new_rand(), @@ -933,17 +941,19 @@ mod tests { fee_calculator: FeeCalculator::default(), })); let invalid_authority = Account::new_data(1, &data, &system_program::ID); - assert_eq!( - check_nonce_account(&invalid_authority.unwrap(), &nonce_pubkey, &blockhash), - Err(CliNonceError::InvalidAuthority.into()), - ); + if let CliError::InvalidNonce(err) = + check_nonce_account(&invalid_authority.unwrap(), &nonce_pubkey, &blockhash).unwrap_err() + { + assert_eq!(err, CliNonceError::InvalidAuthority,); + } let data = Versions::new_current(State::Uninitialized); let invalid_state = Account::new_data(1, &data, &system_program::ID); - assert_eq!( - check_nonce_account(&invalid_state.unwrap(), &nonce_pubkey, &blockhash), - Err(CliNonceError::InvalidStateForOperation.into()), - ); + if let CliError::InvalidNonce(err) = + check_nonce_account(&invalid_state.unwrap(), &nonce_pubkey, &blockhash).unwrap_err() + { + assert_eq!(err, CliNonceError::InvalidStateForOperation,); + } } #[test] diff --git a/cli/src/spend_utils.rs b/cli/src/spend_utils.rs new file mode 100644 index 000000000..eca501122 --- /dev/null +++ b/cli/src/spend_utils.rs @@ -0,0 +1,158 @@ +use crate::{ + checks::{calculate_fee, check_account_for_balance}, + cli::CliError, +}; +use clap::ArgMatches; +use solana_clap_utils::{input_parsers::lamports_of_sol, offline::SIGN_ONLY_ARG}; +use solana_client::rpc_client::RpcClient; +use solana_sdk::{ + fee_calculator::FeeCalculator, message::Message, native_token::lamports_to_sol, pubkey::Pubkey, +}; + +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum SpendAmount { + All, + Some(u64), +} + +impl Default for SpendAmount { + fn default() -> Self { + Self::Some(u64::default()) + } +} + +impl SpendAmount { + pub fn new(amount: Option, sign_only: bool) -> Self { + match amount { + Some(lamports) => Self::Some(lamports), + None if !sign_only => Self::All, + _ => panic!("ALL amount not supported for sign-only operations"), + } + } + + pub fn new_from_matches(matches: &ArgMatches<'_>, name: &str) -> Self { + let amount = lamports_of_sol(matches, name); + let sign_only = matches.is_present(SIGN_ONLY_ARG.name); + SpendAmount::new(amount, sign_only) + } +} + +struct SpendAndFee { + spend: u64, + fee: u64, +} + +pub fn resolve_spend_tx_and_check_account_balance( + rpc_client: &RpcClient, + sign_only: bool, + amount: SpendAmount, + fee_calculator: &FeeCalculator, + from_pubkey: &Pubkey, + build_message: F, +) -> Result<(Message, u64), CliError> +where + F: Fn(u64) -> Message, +{ + resolve_spend_tx_and_check_account_balances( + rpc_client, + sign_only, + amount, + fee_calculator, + from_pubkey, + from_pubkey, + build_message, + ) +} + +pub fn resolve_spend_tx_and_check_account_balances( + rpc_client: &RpcClient, + sign_only: bool, + amount: SpendAmount, + fee_calculator: &FeeCalculator, + from_pubkey: &Pubkey, + fee_pubkey: &Pubkey, + build_message: F, +) -> Result<(Message, u64), CliError> +where + F: Fn(u64) -> Message, +{ + if sign_only { + let (message, SpendAndFee { spend, fee: _ }) = resolve_spend_message( + amount, + fee_calculator, + 0, + from_pubkey, + fee_pubkey, + build_message, + ); + Ok((message, spend)) + } else { + let from_balance = rpc_client.get_balance(&from_pubkey)?; + let (message, SpendAndFee { spend, fee }) = resolve_spend_message( + amount, + fee_calculator, + from_balance, + from_pubkey, + fee_pubkey, + build_message, + ); + if from_pubkey == fee_pubkey { + if from_balance == 0 || from_balance < spend + fee { + return Err(CliError::InsufficientFundsForSpendAndFee( + lamports_to_sol(spend), + lamports_to_sol(fee), + )); + } + } else { + if from_balance < spend { + return Err(CliError::InsufficientFundsForSpend(lamports_to_sol(spend))); + } + if !check_account_for_balance(rpc_client, fee_pubkey, fee)? { + return Err(CliError::InsufficientFundsForFee(lamports_to_sol(fee))); + } + } + Ok((message, spend)) + } +} + +fn resolve_spend_message( + amount: SpendAmount, + fee_calculator: &FeeCalculator, + from_balance: u64, + from_pubkey: &Pubkey, + fee_pubkey: &Pubkey, + build_message: F, +) -> (Message, SpendAndFee) +where + F: Fn(u64) -> Message, +{ + match amount { + SpendAmount::Some(lamports) => { + let message = build_message(lamports); + let fee = calculate_fee(fee_calculator, &[&message]); + ( + message, + SpendAndFee { + spend: lamports, + fee, + }, + ) + } + SpendAmount::All => { + let dummy_message = build_message(0); + let fee = calculate_fee(fee_calculator, &[&dummy_message]); + let lamports = if from_pubkey == fee_pubkey { + from_balance.saturating_sub(fee) + } else { + from_balance + }; + ( + build_message(lamports), + SpendAndFee { + spend: lamports, + fee, + }, + ) + } + } +} diff --git a/cli/src/stake.rs b/cli/src/stake.rs index 3f540e401..aacef4873 100644 --- a/cli/src/stake.rs +++ b/cli/src/stake.rs @@ -1,12 +1,14 @@ use crate::{ + checks::{check_account_for_fee, check_unique_pubkeys}, cli::{ - check_account_for_fee, check_unique_pubkeys, fee_payer_arg, generate_unique_signers, - log_instruction_custom_error, nonce_authority_arg, return_signers, CliCommand, - CliCommandInfo, CliConfig, CliError, ProcessResult, SignerIndex, FEE_PAYER_ARG, + fee_payer_arg, generate_unique_signers, log_instruction_custom_error, nonce_authority_arg, + return_signers, CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult, + SignerIndex, FEE_PAYER_ARG, }, cli_output::{CliStakeHistory, CliStakeHistoryEntry, CliStakeState, CliStakeType}, nonce::{check_nonce_account, nonce_arg, NONCE_ARG, NONCE_AUTHORITY_ARG}, offline::{blockhash_query::BlockhashQuery, *}, + spend_utils::{resolve_spend_tx_and_check_account_balances, SpendAmount}, }; use clap::{App, Arg, ArgGroup, ArgMatches, SubCommand}; use solana_clap_utils::{input_parsers::*, input_validators::*, offline::*, ArgConstant}; @@ -83,9 +85,9 @@ impl StakeSubCommands for App<'_, '_> { .index(2) .value_name("AMOUNT") .takes_value(true) - .validator(is_amount) + .validator(is_amount_or_all) .required(true) - .help("The amount to send to the stake account, in SOL") + .help("The amount to send to the stake account, in SOL; accepts keyword ALL") ) .arg( pubkey!(Arg::with_name("custodian") @@ -393,7 +395,7 @@ pub fn parse_stake_create_account( let custodian = pubkey_of_signer(matches, "custodian", wallet_manager)?.unwrap_or_default(); let staker = pubkey_of_signer(matches, STAKE_AUTHORITY_ARG.name, wallet_manager)?; let withdrawer = pubkey_of_signer(matches, WITHDRAW_AUTHORITY_ARG.name, wallet_manager)?; - let lamports = lamports_of_sol(matches, "amount").unwrap(); + let amount = SpendAmount::new_from_matches(matches, "amount"); 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)?; @@ -422,7 +424,7 @@ pub fn parse_stake_create_account( epoch, unix_timestamp, }, - lamports, + amount, sign_only, blockhash_query, nonce_account, @@ -767,7 +769,7 @@ pub fn process_create_stake_account( staker: &Option, withdrawer: &Option, lockup: &Lockup, - lamports: u64, + amount: SpendAmount, sign_only: bool, blockhash_query: &BlockhashQuery, nonce_account: Option<&Pubkey>, @@ -787,6 +789,59 @@ pub fn process_create_stake_account( (&stake_account_address, "stake_account".to_string()), )?; + let fee_payer = config.signers[fee_payer]; + let nonce_authority = config.signers[nonce_authority]; + + let build_message = |lamports| { + let authorized = Authorized { + staker: staker.unwrap_or(from.pubkey()), + withdrawer: withdrawer.unwrap_or(from.pubkey()), + }; + + let ixs = if let Some(seed) = seed { + stake_instruction::create_account_with_seed( + &from.pubkey(), // from + &stake_account_address, // to + &stake_account.pubkey(), // base + seed, // seed + &authorized, + lockup, + lamports, + ) + } else { + stake_instruction::create_account( + &from.pubkey(), + &stake_account.pubkey(), + &authorized, + lockup, + 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 (recent_blockhash, fee_calculator) = + blockhash_query.get_blockhash_and_fee_calculator(rpc_client)?; + + let (message, lamports) = resolve_spend_tx_and_check_account_balances( + rpc_client, + sign_only, + amount, + &fee_calculator, + &from.pubkey(), + &fee_payer.pubkey(), + build_message, + )?; + if !sign_only { if let Ok(stake_account) = rpc_client.get_account(&stake_account_address) { let err_msg = if stake_account.owner == solana_stake_program::id() { @@ -810,65 +865,19 @@ pub fn process_create_stake_account( )) .into()); } + + 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 authorized = Authorized { - staker: staker.unwrap_or(from.pubkey()), - withdrawer: withdrawer.unwrap_or(from.pubkey()), - }; - - let ixs = if let Some(seed) = seed { - stake_instruction::create_account_with_seed( - &from.pubkey(), // from - &stake_account_address, // to - &stake_account.pubkey(), // base - seed, // seed - &authorized, - lockup, - lamports, - ) - } else { - stake_instruction::create_account( - &from.pubkey(), - &stake_account.pubkey(), - &authorized, - lockup, - lamports, - ) - }; - let (recent_blockhash, fee_calculator) = - blockhash_query.get_blockhash_and_fee_calculator(rpc_client)?; - - let fee_payer = config.signers[fee_payer]; - let nonce_authority = config.signers[nonce_authority]; - - let message = 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 mut tx = Transaction::new_unsigned(message); - if sign_only { tx.try_partial_sign(&config.signers, recent_blockhash)?; return_signers(&tx, &config) } else { 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)?; - } - check_account_for_fee( - rpc_client, - &tx.message.account_keys[0], - &fee_calculator, - &tx.message, - )?; let result = rpc_client.send_and_confirm_transaction_with_spinner(&tx); log_instruction_custom_error::(result, &config) } @@ -2025,7 +2034,7 @@ mod tests { unix_timestamp: 0, custodian, }, - lamports: 50_000_000_000, + amount: SpendAmount::Some(50_000_000_000), sign_only: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), nonce_account: None, @@ -2067,7 +2076,7 @@ mod tests { staker: None, withdrawer: None, lockup: Lockup::default(), - lamports: 50_000_000_000, + amount: SpendAmount::Some(50_000_000_000), sign_only: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), nonce_account: None, @@ -2125,7 +2134,7 @@ mod tests { staker: None, withdrawer: None, lockup: Lockup::default(), - lamports: 50_000_000_000, + amount: SpendAmount::Some(50_000_000_000), sign_only: false, blockhash_query: BlockhashQuery::FeeCalculator( blockhash_query::Source::NonceAccount(nonce_account), diff --git a/cli/src/storage.rs b/cli/src/storage.rs index 377456f0b..c33c71c5b 100644 --- a/cli/src/storage.rs +++ b/cli/src/storage.rs @@ -1,7 +1,9 @@ -use crate::cli::{ - check_account_for_fee, check_unique_pubkeys, generate_unique_signers, - log_instruction_custom_error, CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult, - SignerIndex, +use crate::{ + checks::{check_account_for_fee, check_unique_pubkeys}, + cli::{ + generate_unique_signers, log_instruction_custom_error, CliCommand, CliCommandInfo, + CliConfig, CliError, ProcessResult, SignerIndex, + }, }; use clap::{App, Arg, ArgMatches, SubCommand}; use solana_clap_utils::{input_parsers::*, input_validators::*, keypair::signer_from_path}; diff --git a/cli/src/test_utils.rs b/cli/src/test_utils.rs index abc57db99..2a23174e3 100644 --- a/cli/src/test_utils.rs +++ b/cli/src/test_utils.rs @@ -4,7 +4,7 @@ use std::{thread::sleep, time::Duration}; pub 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(); + let balance = client.get_balance(pubkey).unwrap(); if balance == expected_balance { return; } diff --git a/cli/src/validator_info.rs b/cli/src/validator_info.rs index 4f2927e7d..719294ebd 100644 --- a/cli/src/validator_info.rs +++ b/cli/src/validator_info.rs @@ -1,6 +1,7 @@ use crate::{ - cli::{check_account_for_fee, CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult}, + cli::{CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult}, cli_output::{CliValidatorInfo, CliValidatorInfoVec}, + spend_utils::{resolve_spend_tx_and_check_account_balance, SpendAmount}, }; use bincode::deserialize; use clap::{App, AppSettings, Arg, ArgMatches, SubCommand}; @@ -310,8 +311,10 @@ pub fn process_set_validator_info( .poll_get_balance_with_commitment(&info_pubkey, CommitmentConfig::default()) .unwrap_or(0); - let keys = vec![(id(), false), (config.signers[0].pubkey(), true)]; - let (message, signers): (Message, Vec<&dyn Signer>) = if balance == 0 { + let lamports = + rpc_client.get_minimum_balance_for_rent_exemption(ValidatorInfo::max_space() as usize)?; + + let signers = if balance == 0 { if info_pubkey != info_keypair.pubkey() { println!( "Account {:?} does not exist. Generating new keypair...", @@ -319,54 +322,59 @@ pub fn process_set_validator_info( ); info_pubkey = info_keypair.pubkey(); } - println!( - "Publishing info for Validator {:?}", - config.signers[0].pubkey() - ); - let lamports = rpc_client - .get_minimum_balance_for_rent_exemption(ValidatorInfo::max_space() as usize)?; - let mut instructions = config_instruction::create_account::( - &config.signers[0].pubkey(), - &info_keypair.pubkey(), - lamports, - keys.clone(), - ); - instructions.extend_from_slice(&[config_instruction::store( - &info_keypair.pubkey(), - true, - keys, - &validator_info, - )]); - let signers = vec![config.signers[0], &info_keypair]; - let message = Message::new(&instructions); - (message, signers) + vec![config.signers[0], &info_keypair] } else { - println!( - "Updating Validator {:?} info at: {:?}", - config.signers[0].pubkey(), - info_pubkey - ); - let instructions = vec![config_instruction::store( - &info_pubkey, - false, - keys, - &validator_info, - )]; - let message = Message::new_with_payer(&instructions, Some(&config.signers[0].pubkey())); - let signers = vec![config.signers[0]]; - (message, signers) + vec![config.signers[0]] + }; + + let build_message = |lamports| { + let keys = vec![(id(), false), (config.signers[0].pubkey(), true)]; + if balance == 0 { + println!( + "Publishing info for Validator {:?}", + config.signers[0].pubkey() + ); + let mut instructions = config_instruction::create_account::( + &config.signers[0].pubkey(), + &info_pubkey, + lamports, + keys.clone(), + ); + instructions.extend_from_slice(&[config_instruction::store( + &info_pubkey, + true, + keys, + &validator_info, + )]); + Message::new(&instructions) + } else { + println!( + "Updating Validator {:?} info at: {:?}", + config.signers[0].pubkey(), + info_pubkey + ); + let instructions = vec![config_instruction::store( + &info_pubkey, + false, + keys, + &validator_info, + )]; + Message::new_with_payer(&instructions, Some(&config.signers[0].pubkey())) + } }; // Submit transaction let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; + let (message, _) = resolve_spend_tx_and_check_account_balance( + rpc_client, + false, + SpendAmount::Some(lamports), + &fee_calculator, + &config.signers[0].pubkey(), + build_message, + )?; let mut tx = Transaction::new_unsigned(message); tx.try_sign(&signers, recent_blockhash)?; - check_account_for_fee( - rpc_client, - &config.signers[0].pubkey(), - &fee_calculator, - &tx.message, - )?; let signature_str = rpc_client.send_and_confirm_transaction_with_spinner(&tx)?; println!("Success! Validator info published at: {:?}", info_pubkey); diff --git a/cli/src/vote.rs b/cli/src/vote.rs index 07c8060ed..dc1da1b81 100644 --- a/cli/src/vote.rs +++ b/cli/src/vote.rs @@ -1,10 +1,11 @@ use crate::{ + checks::{check_account_for_fee, check_unique_pubkeys}, cli::{ - check_account_for_fee, check_unique_pubkeys, generate_unique_signers, - log_instruction_custom_error, CliCommand, CliCommandInfo, CliConfig, CliError, - ProcessResult, SignerIndex, + generate_unique_signers, log_instruction_custom_error, CliCommand, CliCommandInfo, + CliConfig, CliError, ProcessResult, SignerIndex, }, cli_output::{CliEpochVotingHistory, CliLockout, CliVoteAccount}, + spend_utils::{resolve_spend_tx_and_check_account_balance, SpendAmount}, }; use clap::{value_t_or_exit, App, Arg, ArgMatches, SubCommand}; use solana_clap_utils::{ @@ -386,6 +387,39 @@ pub fn process_create_vote_account( (&identity_pubkey, "identity_pubkey".to_string()), )?; + let required_balance = rpc_client + .get_minimum_balance_for_rent_exemption(VoteState::size_of())? + .max(1); + let amount = SpendAmount::Some(required_balance); + + let build_message = |lamports| { + let vote_init = VoteInit { + node_pubkey: identity_pubkey, + authorized_voter: authorized_voter.unwrap_or(identity_pubkey), + authorized_withdrawer: authorized_withdrawer.unwrap_or(identity_pubkey), + commission, + }; + + let ixs = if let Some(seed) = seed { + vote_instruction::create_account_with_seed( + &config.signers[0].pubkey(), // from + &vote_account_address, // to + &vote_account_pubkey, // base + seed, // seed + &vote_init, + lamports, + ) + } else { + vote_instruction::create_account( + &config.signers[0].pubkey(), + &vote_account_pubkey, + &vote_init, + lamports, + ) + }; + Message::new(&ixs) + }; + if let Ok(vote_account) = rpc_client.get_account(&vote_account_address) { let err_msg = if vote_account.owner == solana_vote_program::id() { format!("Vote account {} already exists", vote_account_address) @@ -398,45 +432,18 @@ pub fn process_create_vote_account( return Err(CliError::BadParameter(err_msg).into()); } - let required_balance = rpc_client - .get_minimum_balance_for_rent_exemption(VoteState::size_of())? - .max(1); - - let vote_init = VoteInit { - node_pubkey: identity_pubkey, - authorized_voter: authorized_voter.unwrap_or(identity_pubkey), - authorized_withdrawer: authorized_withdrawer.unwrap_or(identity_pubkey), - commission, - }; - - let ixs = if let Some(seed) = seed { - vote_instruction::create_account_with_seed( - &config.signers[0].pubkey(), // from - &vote_account_address, // to - &vote_account_pubkey, // base - seed, // seed - &vote_init, - required_balance, - ) - } else { - vote_instruction::create_account( - &config.signers[0].pubkey(), - &vote_account_pubkey, - &vote_init, - required_balance, - ) - }; let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; - let message = Message::new(&ixs); + let (message, _) = resolve_spend_tx_and_check_account_balance( + rpc_client, + false, + amount, + &fee_calculator, + &config.signers[0].pubkey(), + build_message, + )?; let mut tx = Transaction::new_unsigned(message); tx.try_sign(&config.signers, recent_blockhash)?; - check_account_for_fee( - rpc_client, - &config.signers[0].pubkey(), - &fee_calculator, - &tx.message, - )?; let result = rpc_client.send_and_confirm_transaction_with_spinner(&tx); log_instruction_custom_error::(result, &config) } diff --git a/cli/tests/nonce.rs b/cli/tests/nonce.rs index 3b11a1b22..5934452f6 100644 --- a/cli/tests/nonce.rs +++ b/cli/tests/nonce.rs @@ -7,6 +7,7 @@ use solana_cli::{ blockhash_query::{self, BlockhashQuery}, parse_sign_only_reply_string, }, + spend_utils::SpendAmount, }; use solana_client::rpc_client::RpcClient; use solana_core::contact_info::ContactInfo; @@ -126,7 +127,7 @@ fn full_battery_tests( nonce_account: 1, seed, nonce_authority: optional_authority, - lamports: 1000, + amount: SpendAmount::Some(1000), }; process_command(&config_payer).unwrap(); @@ -289,7 +290,7 @@ fn test_create_account_with_seed() { nonce_account: 0, seed: Some(seed), nonce_authority: Some(authority_pubkey), - lamports: 241, + amount: SpendAmount::Some(241), }; process_command(&creator_config).unwrap(); check_balance(241, &rpc_client, &nonce_address); @@ -311,7 +312,7 @@ fn test_create_account_with_seed() { authority_config.command = CliCommand::ClusterVersion; process_command(&authority_config).unwrap_err(); authority_config.command = CliCommand::Transfer { - lamports: 10, + amount: SpendAmount::Some(10), to: to_address, from: 0, sign_only: true, @@ -333,7 +334,7 @@ fn test_create_account_with_seed() { format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); submit_config.signers = vec![&authority_presigner]; submit_config.command = CliCommand::Transfer { - lamports: 10, + amount: SpendAmount::Some(10), to: to_address, from: 0, sign_only: false, diff --git a/cli/tests/pay.rs b/cli/tests/pay.rs index 583333268..a82fa9ef7 100644 --- a/cli/tests/pay.rs +++ b/cli/tests/pay.rs @@ -9,6 +9,7 @@ use solana_cli::{ blockhash_query::{self, BlockhashQuery}, parse_sign_only_reply_string, }, + spend_utils::SpendAmount, }; use solana_client::rpc_client::RpcClient; use solana_core::validator::TestValidator; @@ -76,7 +77,7 @@ fn test_cli_timestamp_tx() { let date_string = "\"2018-09-19T17:30:59Z\""; let dt: DateTime = serde_json::from_str(&date_string).unwrap(); config_payer.command = CliCommand::Pay(PayCommand { - lamports: 10, + amount: SpendAmount::Some(10), to: bob_pubkey, timestamp: Some(dt), timestamp_pubkey: Some(config_witness.signers[0].pubkey()), @@ -159,7 +160,7 @@ fn test_cli_witness_tx() { // Make transaction (from config_payer to bob_pubkey) requiring witness signature from config_witness config_payer.command = CliCommand::Pay(PayCommand { - lamports: 10, + amount: SpendAmount::Some(10), to: bob_pubkey, witnesses: Some(vec![config_witness.signers[0].pubkey()]), ..PayCommand::default() @@ -233,7 +234,7 @@ fn test_cli_cancel_tx() { // Make transaction (from config_payer to bob_pubkey) requiring witness signature from config_witness config_payer.command = CliCommand::Pay(PayCommand { - lamports: 10, + amount: SpendAmount::Some(10), to: bob_pubkey, witnesses: Some(vec![config_witness.signers[0].pubkey()]), cancelable: true, @@ -318,7 +319,7 @@ fn test_offline_pay_tx() { let (blockhash, _) = rpc_client.get_recent_blockhash().unwrap(); config_offline.command = CliCommand::Pay(PayCommand { - lamports: 10, + amount: SpendAmount::Some(10), to: bob_pubkey, blockhash_query: BlockhashQuery::None(blockhash), sign_only: true, @@ -339,7 +340,7 @@ fn test_offline_pay_tx() { let online_pubkey = config_online.signers[0].pubkey(); config_online.signers = vec![&offline_presigner]; config_online.command = CliCommand::Pay(PayCommand { - lamports: 10, + amount: SpendAmount::Some(10), to: bob_pubkey, blockhash_query: BlockhashQuery::FeeCalculator(blockhash_query::Source::Cluster, blockhash), ..PayCommand::default() @@ -400,7 +401,7 @@ fn test_nonced_pay_tx() { nonce_account: 1, seed: None, nonce_authority: Some(config.signers[0].pubkey()), - lamports: minimum_nonce_balance, + amount: SpendAmount::Some(minimum_nonce_balance), }; config.signers.push(&nonce_account); process_command(&config).unwrap(); @@ -417,7 +418,7 @@ fn test_nonced_pay_tx() { let bob_pubkey = Pubkey::new_rand(); config.signers = vec![&default_signer]; config.command = CliCommand::Pay(PayCommand { - lamports: 10, + amount: SpendAmount::Some(10), to: bob_pubkey, blockhash_query: BlockhashQuery::FeeCalculator( blockhash_query::Source::NonceAccount(nonce_account.pubkey()), diff --git a/cli/tests/request_airdrop.rs b/cli/tests/request_airdrop.rs index a7b5614ae..d699f4eb9 100644 --- a/cli/tests/request_airdrop.rs +++ b/cli/tests/request_airdrop.rs @@ -35,8 +35,7 @@ fn test_cli_request_airdrop() { let rpc_client = RpcClient::new_socket(leader_data.rpc); let balance = rpc_client - .retry_get_balance(&bob_config.signers[0].pubkey(), 1) - .unwrap() + .get_balance(&bob_config.signers[0].pubkey()) .unwrap(); assert_eq!(balance, 50); diff --git a/cli/tests/stake.rs b/cli/tests/stake.rs index 1323d4a9d..9a9098084 100644 --- a/cli/tests/stake.rs +++ b/cli/tests/stake.rs @@ -7,6 +7,7 @@ use solana_cli::{ blockhash_query::{self, BlockhashQuery}, parse_sign_only_reply_string, }, + spend_utils::SpendAmount, }; use solana_client::rpc_client::RpcClient; use solana_core::validator::{TestValidator, TestValidatorOptions}; @@ -73,7 +74,7 @@ fn test_stake_delegation_force() { staker: None, withdrawer: None, lockup: Lockup::default(), - lamports: 50_000, + amount: SpendAmount::Some(50_000), sign_only: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), nonce_account: None, @@ -165,7 +166,7 @@ fn test_seed_stake_delegation_and_deactivation() { staker: None, withdrawer: None, lockup: Lockup::default(), - lamports: 50_000, + amount: SpendAmount::Some(50_000), sign_only: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), nonce_account: None, @@ -249,7 +250,7 @@ fn test_stake_delegation_and_deactivation() { staker: None, withdrawer: None, lockup: Lockup::default(), - lamports: 50_000, + amount: SpendAmount::Some(50_000), sign_only: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), nonce_account: None, @@ -356,7 +357,7 @@ fn test_offline_stake_delegation_and_deactivation() { staker: Some(config_offline.signers[0].pubkey().into()), withdrawer: None, lockup: Lockup::default(), - lamports: 50_000, + amount: SpendAmount::Some(50_000), sign_only: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), nonce_account: None, @@ -478,7 +479,7 @@ fn test_nonced_stake_delegation_and_deactivation() { staker: None, withdrawer: None, lockup: Lockup::default(), - lamports: 50_000, + amount: SpendAmount::Some(50_000), sign_only: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), nonce_account: None, @@ -495,7 +496,7 @@ fn test_nonced_stake_delegation_and_deactivation() { nonce_account: 1, seed: None, nonce_authority: Some(config.signers[0].pubkey()), - lamports: minimum_nonce_balance, + amount: SpendAmount::Some(minimum_nonce_balance), }; process_command(&config).unwrap(); @@ -607,7 +608,7 @@ fn test_stake_authorize() { staker: None, withdrawer: None, lockup: Lockup::default(), - lamports: 50_000, + amount: SpendAmount::Some(50_000), sign_only: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), nonce_account: None, @@ -735,7 +736,7 @@ fn test_stake_authorize() { nonce_account: 1, seed: None, nonce_authority: Some(offline_authority_pubkey), - lamports: minimum_nonce_balance, + amount: SpendAmount::Some(minimum_nonce_balance), }; process_command(&config).unwrap(); @@ -862,7 +863,7 @@ fn test_stake_authorize_with_fee_payer() { staker: None, withdrawer: None, lockup: Lockup::default(), - lamports: 50_000, + amount: SpendAmount::Some(50_000), sign_only: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), nonce_account: None, @@ -991,7 +992,7 @@ fn test_stake_split() { staker: Some(offline_pubkey), withdrawer: Some(offline_pubkey), lockup: Lockup::default(), - lamports: 10 * minimum_stake_balance, + amount: SpendAmount::Some(10 * minimum_stake_balance), sign_only: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), nonce_account: None, @@ -1016,7 +1017,7 @@ fn test_stake_split() { nonce_account: 1, seed: None, nonce_authority: Some(offline_pubkey), - lamports: minimum_nonce_balance, + amount: SpendAmount::Some(minimum_nonce_balance), }; process_command(&config).unwrap(); check_balance(minimum_nonce_balance, &rpc_client, &nonce_account.pubkey()); @@ -1147,7 +1148,7 @@ fn test_stake_set_lockup() { staker: Some(offline_pubkey), withdrawer: Some(offline_pubkey), lockup, - lamports: 10 * minimum_stake_balance, + amount: SpendAmount::Some(10 * minimum_stake_balance), sign_only: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), nonce_account: None, @@ -1273,7 +1274,7 @@ fn test_stake_set_lockup() { nonce_account: 1, seed: None, nonce_authority: Some(offline_pubkey), - lamports: minimum_nonce_balance, + amount: SpendAmount::Some(minimum_nonce_balance), }; process_command(&config).unwrap(); check_balance(minimum_nonce_balance, &rpc_client, &nonce_account_pubkey); @@ -1393,7 +1394,7 @@ fn test_offline_nonced_create_stake_account_and_withdraw() { nonce_account: 1, seed: None, nonce_authority: Some(offline_pubkey), - lamports: minimum_nonce_balance, + amount: SpendAmount::Some(minimum_nonce_balance), }; process_command(&config).unwrap(); @@ -1413,7 +1414,7 @@ fn test_offline_nonced_create_stake_account_and_withdraw() { staker: None, withdrawer: None, lockup: Lockup::default(), - lamports: 50_000, + amount: SpendAmount::Some(50_000), sign_only: true, blockhash_query: BlockhashQuery::None(nonce_hash), nonce_account: Some(nonce_pubkey), @@ -1434,7 +1435,7 @@ fn test_offline_nonced_create_stake_account_and_withdraw() { staker: Some(offline_pubkey), withdrawer: None, lockup: Lockup::default(), - lamports: 50_000, + amount: SpendAmount::Some(50_000), sign_only: false, blockhash_query: BlockhashQuery::FeeCalculator( blockhash_query::Source::NonceAccount(nonce_pubkey), @@ -1507,7 +1508,7 @@ fn test_offline_nonced_create_stake_account_and_withdraw() { staker: None, withdrawer: None, lockup: Lockup::default(), - lamports: 50_000, + amount: SpendAmount::Some(50_000), sign_only: true, blockhash_query: BlockhashQuery::None(nonce_hash), nonce_account: Some(nonce_pubkey), @@ -1526,7 +1527,7 @@ fn test_offline_nonced_create_stake_account_and_withdraw() { staker: Some(offline_pubkey.into()), withdrawer: Some(offline_pubkey.into()), lockup: Lockup::default(), - lamports: 50_000, + amount: SpendAmount::Some(50_000), sign_only: false, blockhash_query: BlockhashQuery::FeeCalculator( blockhash_query::Source::NonceAccount(nonce_pubkey), diff --git a/cli/tests/transfer.rs b/cli/tests/transfer.rs index 2fb4fe70c..8c7641610 100644 --- a/cli/tests/transfer.rs +++ b/cli/tests/transfer.rs @@ -7,6 +7,7 @@ use solana_cli::{ blockhash_query::{self, BlockhashQuery}, parse_sign_only_reply_string, }, + spend_utils::SpendAmount, }; use solana_client::rpc_client::RpcClient; use solana_core::validator::{TestValidator, TestValidatorOptions}; @@ -55,7 +56,7 @@ fn test_transfer() { // Plain ole transfer config.command = CliCommand::Transfer { - lamports: 10, + amount: SpendAmount::Some(10), to: recipient_pubkey, from: 0, sign_only: false, @@ -69,6 +70,22 @@ fn test_transfer() { check_balance(49_989, &rpc_client, &sender_pubkey); check_balance(10, &rpc_client, &recipient_pubkey); + // Plain ole transfer, failure due to InsufficientFundsForSpendAndFee + config.command = CliCommand::Transfer { + amount: SpendAmount::Some(49_989), + to: recipient_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, + }; + assert!(process_command(&config).is_err()); + check_balance(49_989, &rpc_client, &sender_pubkey); + check_balance(10, &rpc_client, &recipient_pubkey); + let mut offline = CliConfig::default(); offline.json_rpc_url = String::default(); offline.signers = vec![&default_offline_signer]; @@ -83,7 +100,7 @@ fn test_transfer() { // Offline transfer let (blockhash, _) = rpc_client.get_recent_blockhash().unwrap(); offline.command = CliCommand::Transfer { - lamports: 10, + amount: SpendAmount::Some(10), to: recipient_pubkey, from: 0, sign_only: true, @@ -100,7 +117,7 @@ fn test_transfer() { let offline_presigner = sign_only.presigner_of(&offline_pubkey).unwrap(); config.signers = vec![&offline_presigner]; config.command = CliCommand::Transfer { - lamports: 10, + amount: SpendAmount::Some(10), to: recipient_pubkey, from: 0, sign_only: false, @@ -124,7 +141,7 @@ fn test_transfer() { nonce_account: 1, seed: None, nonce_authority: None, - lamports: minimum_nonce_balance, + amount: SpendAmount::Some(minimum_nonce_balance), }; process_command(&config).unwrap(); check_balance(49_987 - minimum_nonce_balance, &rpc_client, &sender_pubkey); @@ -138,7 +155,7 @@ fn test_transfer() { // Nonced transfer config.signers = vec![&default_signer]; config.command = CliCommand::Transfer { - lamports: 10, + amount: SpendAmount::Some(10), to: recipient_pubkey, from: 0, sign_only: false, @@ -179,7 +196,7 @@ fn test_transfer() { // Offline, nonced transfer offline.signers = vec![&default_offline_signer]; offline.command = CliCommand::Transfer { - lamports: 10, + amount: SpendAmount::Some(10), to: recipient_pubkey, from: 0, sign_only: true, @@ -195,7 +212,7 @@ fn test_transfer() { let offline_presigner = sign_only.presigner_of(&offline_pubkey).unwrap(); config.signers = vec![&offline_presigner]; config.command = CliCommand::Transfer { - lamports: 10, + amount: SpendAmount::Some(10), to: recipient_pubkey, from: 0, sign_only: false, @@ -272,7 +289,7 @@ fn test_transfer_multisession_signing() { fee_payer_config.command = CliCommand::ClusterVersion; process_command(&fee_payer_config).unwrap_err(); fee_payer_config.command = CliCommand::Transfer { - lamports: 42, + amount: SpendAmount::Some(42), to: to_pubkey, from: 1, sign_only: true, @@ -298,7 +315,7 @@ fn test_transfer_multisession_signing() { from_config.command = CliCommand::ClusterVersion; process_command(&from_config).unwrap_err(); from_config.command = CliCommand::Transfer { - lamports: 42, + amount: SpendAmount::Some(42), to: to_pubkey, from: 1, sign_only: true, @@ -321,7 +338,7 @@ fn test_transfer_multisession_signing() { config.json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); config.signers = vec![&fee_payer_presigner, &from_presigner]; config.command = CliCommand::Transfer { - lamports: 42, + amount: SpendAmount::Some(42), to: to_pubkey, from: 1, sign_only: false, @@ -340,3 +357,57 @@ fn test_transfer_multisession_signing() { server.close().unwrap(); remove_dir_all(ledger_path).unwrap(); } + +#[test] +fn test_transfer_all() { + let TestValidator { + server, + leader_data, + alice: mint_keypair, + ledger_path, + .. + } = TestValidator::run_with_options(TestValidatorOptions { + fees: 1, + bootstrap_validator_lamports: 42_000, + ..TestValidatorOptions::default() + }); + + let (sender, receiver) = channel(); + run_local_faucet(mint_keypair, sender, None); + let faucet_addr = receiver.recv().unwrap(); + + let rpc_client = RpcClient::new_socket(leader_data.rpc); + + let default_signer = Keypair::new(); + + let mut config = CliConfig::default(); + config.json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); + config.signers = vec![&default_signer]; + + let sender_pubkey = config.signers[0].pubkey(); + let recipient_pubkey = Pubkey::new(&[1u8; 32]); + + request_and_confirm_airdrop(&rpc_client, &faucet_addr, &sender_pubkey, 50_000, &config) + .unwrap(); + check_balance(50_000, &rpc_client, &sender_pubkey); + check_balance(0, &rpc_client, &recipient_pubkey); + + // Plain ole transfer + config.command = CliCommand::Transfer { + amount: SpendAmount::All, + to: recipient_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, + }; + process_command(&config).unwrap(); + check_balance(0, &rpc_client, &sender_pubkey); + check_balance(49_999, &rpc_client, &recipient_pubkey); + + server.close().unwrap(); + remove_dir_all(ledger_path).unwrap(); +} diff --git a/client/src/mock_rpc_client_request.rs b/client/src/mock_rpc_client_request.rs index 6fbd52f12..17073c738 100644 --- a/client/src/mock_rpc_client_request.rs +++ b/client/src/mock_rpc_client_request.rs @@ -117,7 +117,7 @@ impl GenericRpcClientRequest for MockRpcClientRequest { }; Value::String(signature) } - RpcRequest::GetMinimumBalanceForRentExemption => Value::Number(Number::from(1234)), + RpcRequest::GetMinimumBalanceForRentExemption => Value::Number(Number::from(20)), _ => Value::Null, }; Ok(val) diff --git a/client/src/rpc_client.rs b/client/src/rpc_client.rs index 326359692..83ba77b7c 100644 --- a/client/src/rpc_client.rs +++ b/client/src/rpc_client.rs @@ -499,24 +499,6 @@ impl RpcClient { Ok(()) } - pub fn retry_get_balance( - &self, - pubkey: &Pubkey, - retries: usize, - ) -> Result, Box> { - let request = RpcRequest::GetBalance; - let balance_json = self - .client - .send(request, json!([pubkey.to_string()]), retries) - .map_err(|err| err.into_with_request(request))?; - - Ok(Some( - serde_json::from_value::>(balance_json) - .map_err(|err| ClientError::new_with_request(err.into(), request))? - .value, - )) - } - pub fn get_account(&self, pubkey: &Pubkey) -> ClientResult { self.get_account_with_commitment(pubkey, CommitmentConfig::default())? .value diff --git a/install/src/command.rs b/install/src/command.rs index a74ed828f..0dac49b73 100644 --- a/install/src/command.rs +++ b/install/src/command.rs @@ -664,7 +664,7 @@ pub fn deploy( let progress_bar = new_spinner_progress_bar(); progress_bar.set_message(&format!("{}Checking cluster...", LOOKING_GLASS)); let balance = rpc_client - .retry_get_balance(&from_keypair.pubkey(), 5) + .get_balance(&from_keypair.pubkey()) .map_err(|err| { format!( "Unable to get the account balance of {}: {}", @@ -672,7 +672,7 @@ pub fn deploy( ) })?; progress_bar.finish_and_clear(); - if balance.unwrap_or(0) == 0 { + if balance == 0 { return Err(format!("{} account balance is empty", from_keypair_file)); } diff --git a/sdk/src/system_instruction.rs b/sdk/src/system_instruction.rs index f1dc3c43a..09d384bcf 100644 --- a/sdk/src/system_instruction.rs +++ b/sdk/src/system_instruction.rs @@ -13,7 +13,7 @@ use thiserror::Error; pub enum SystemError { #[error("an account with the same address already exists")] AccountAlreadyInUse, - #[error("account does not have enough lamports to perform the operation")] + #[error("account does not have enough SOL to perform the operation")] ResultWithNegativeLamports, #[error("cannot assign account to this program id")] InvalidProgramId,