diff --git a/Cargo.lock b/Cargo.lock index f69bf39968..ea48472215 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3591,6 +3591,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "solana-clap-utils", "solana-client", "solana-sdk 1.4.0", "solana-stake-program", diff --git a/clap-utils/src/keypair.rs b/clap-utils/src/keypair.rs index cf0324b7a6..5d65d88190 100644 --- a/clap-utils/src/keypair.rs +++ b/clap-utils/src/keypair.rs @@ -11,6 +11,7 @@ use solana_remote_wallet::{ remote_wallet::{maybe_wallet_manager, RemoteWalletError, RemoteWalletManager}, }; use solana_sdk::{ + hash::Hash, pubkey::Pubkey, signature::{ keypair_from_seed, keypair_from_seed_phrase_and_passphrase, read_keypair, @@ -25,6 +26,22 @@ use std::{ sync::Arc, }; +pub struct SignOnly { + pub blockhash: Hash, + pub present_signers: Vec<(Pubkey, Signature)>, + pub absent_signers: Vec, + pub bad_signers: Vec, +} + +impl SignOnly { + pub fn has_all_signers(&self) -> bool { + self.absent_signers.is_empty() && self.bad_signers.is_empty() + } + + pub fn presigner_of(&self, pubkey: &Pubkey) -> Option { + presigner_from_pubkey_sigs(pubkey, &self.present_signers) + } +} pub type CliSigners = Vec>; pub type SignerIndex = usize; pub struct CliSignerInfo { diff --git a/cli-output/Cargo.toml b/cli-output/Cargo.toml index 0b0afa06c1..dbe70d465e 100644 --- a/cli-output/Cargo.toml +++ b/cli-output/Cargo.toml @@ -17,6 +17,7 @@ indicatif = "0.15.0" serde = "1.0.112" serde_derive = "1.0.103" serde_json = "1.0.56" +solana-clap-utils = { path = "../clap-utils", version = "1.4.0" } solana-client = { path = "../client", version = "1.4.0" } solana-sdk = { path = "../sdk", version = "1.4.0" } solana-stake-program = { path = "../programs/stake", version = "1.4.0" } diff --git a/cli-output/src/cli_output.rs b/cli-output/src/cli_output.rs index c3beb34ffe..f5f3f20c3a 100644 --- a/cli-output/src/cli_output.rs +++ b/cli-output/src/cli_output.rs @@ -4,14 +4,19 @@ use console::{style, Emoji}; use inflector::cases::titlecase::to_title_case; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; +use solana_clap_utils::keypair::SignOnly; use solana_client::rpc_response::{ RpcAccountBalance, RpcKeyedAccount, RpcSupply, RpcVoteAccountInfo, }; use solana_sdk::{ clock::{self, Epoch, Slot, UnixTimestamp}, epoch_info::EpochInfo, + hash::Hash, native_token::lamports_to_sol, + pubkey::Pubkey, + signature::Signature, stake_history::StakeHistoryEntry, + transaction::Transaction, }; use solana_stake_program::stake_state::{Authorized, Lockup}; use solana_vote_program::{ @@ -21,6 +26,7 @@ use solana_vote_program::{ use std::{ collections::{BTreeMap, HashMap}, fmt, + str::FromStr, time::Duration, }; @@ -1142,3 +1148,149 @@ impl fmt::Display for CliFees { Ok(()) } } + +pub fn return_signers( + tx: &Transaction, + output_format: &OutputFormat, +) -> Result> { + let verify_results = tx.verify_with_results(); + let mut signers = Vec::new(); + let mut absent = Vec::new(); + let mut bad_sig = Vec::new(); + tx.signatures + .iter() + .zip(tx.message.account_keys.iter()) + .zip(verify_results.into_iter()) + .for_each(|((sig, key), res)| { + if res { + signers.push(format!("{}={}", key, sig)) + } else if *sig == Signature::default() { + absent.push(key.to_string()); + } else { + bad_sig.push(key.to_string()); + } + }); + + let cli_command = CliSignOnlyData { + blockhash: tx.message.recent_blockhash.to_string(), + signers, + absent, + bad_sig, + }; + + Ok(output_format.formatted_string(&cli_command)) +} + +pub fn parse_sign_only_reply_string(reply: &str) -> SignOnly { + let object: Value = serde_json::from_str(&reply).unwrap(); + let blockhash_str = object.get("blockhash").unwrap().as_str().unwrap(); + let blockhash = blockhash_str.parse::().unwrap(); + let mut present_signers: Vec<(Pubkey, Signature)> = Vec::new(); + let signer_strings = object.get("signers"); + if let Some(sig_strings) = signer_strings { + present_signers = sig_strings + .as_array() + .unwrap() + .iter() + .map(|signer_string| { + let mut signer = signer_string.as_str().unwrap().split('='); + let key = Pubkey::from_str(signer.next().unwrap()).unwrap(); + let sig = Signature::from_str(signer.next().unwrap()).unwrap(); + (key, sig) + }) + .collect(); + } + let mut absent_signers: Vec = Vec::new(); + let signer_strings = object.get("absent"); + if let Some(sig_strings) = signer_strings { + absent_signers = sig_strings + .as_array() + .unwrap() + .iter() + .map(|val| { + let s = val.as_str().unwrap(); + Pubkey::from_str(s).unwrap() + }) + .collect(); + } + let mut bad_signers: Vec = Vec::new(); + let signer_strings = object.get("badSig"); + if let Some(sig_strings) = signer_strings { + bad_signers = sig_strings + .as_array() + .unwrap() + .iter() + .map(|val| { + let s = val.as_str().unwrap(); + Pubkey::from_str(s).unwrap() + }) + .collect(); + } + + SignOnly { + blockhash, + present_signers, + absent_signers, + bad_signers, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use solana_sdk::{ + message::Message, + pubkey::Pubkey, + signature::{keypair_from_seed, NullSigner, Signature, Signer, SignerError}, + system_instruction, + transaction::Transaction, + }; + + #[test] + fn test_return_signers() { + struct BadSigner { + pubkey: Pubkey, + } + + impl BadSigner { + pub fn new(pubkey: Pubkey) -> Self { + Self { pubkey } + } + } + + impl Signer for BadSigner { + fn try_pubkey(&self) -> Result { + Ok(self.pubkey) + } + + fn try_sign_message(&self, _message: &[u8]) -> Result { + Ok(Signature::new(&[1u8; 64])) + } + } + + let present: Box = Box::new(keypair_from_seed(&[2u8; 32]).unwrap()); + let absent: Box = Box::new(NullSigner::new(&Pubkey::new(&[3u8; 32]))); + let bad: Box = Box::new(BadSigner::new(Pubkey::new(&[4u8; 32]))); + let to = Pubkey::new(&[5u8; 32]); + let nonce = Pubkey::new(&[6u8; 32]); + let from = present.pubkey(); + let fee_payer = absent.pubkey(); + let nonce_auth = bad.pubkey(); + let mut tx = Transaction::new_unsigned(Message::new_with_nonce( + vec![system_instruction::transfer(&from, &to, 42)], + Some(&fee_payer), + &nonce, + &nonce_auth, + )); + + let signers = vec![present.as_ref(), absent.as_ref(), bad.as_ref()]; + let blockhash = Hash::new(&[7u8; 32]); + tx.try_partial_sign(&signers, blockhash).unwrap(); + let res = return_signers(&tx, &OutputFormat::JsonCompact).unwrap(); + let sign_only = parse_sign_only_reply_string(&res); + assert_eq!(sign_only.blockhash, blockhash); + assert_eq!(sign_only.present_signers[0].0, present.pubkey()); + assert_eq!(sign_only.absent_signers[0], absent.pubkey()); + assert_eq!(sign_only.bad_signers[0], bad.pubkey()); + } +} diff --git a/cli/src/cli.rs b/cli/src/cli.rs index d6ec6a8d2b..084cd8e247 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -1,6 +1,5 @@ use crate::{ - checks::*, cluster_query::*, nonce::*, offline::return_signers, spend_utils::*, stake::*, - validator_info::*, vote::*, + checks::*, cluster_query::*, nonce::*, spend_utils::*, stake::*, validator_info::*, vote::*, }; use clap::{value_t_or_exit, App, AppSettings, Arg, ArgMatches, SubCommand}; use log::*; @@ -21,7 +20,7 @@ use solana_cli_output::{ display::{ build_balance_message, new_spinner_progress_bar, println_name_value, println_transaction, }, - CliAccount, CliSignature, OutputFormat, + return_signers, CliAccount, CliSignature, OutputFormat, }; use solana_client::{ blockhash_query::BlockhashQuery, diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 6cac10cc9c..0dfbbe841a 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -24,7 +24,6 @@ pub mod checks; pub mod cli; pub mod cluster_query; pub mod nonce; -pub mod offline; pub mod spend_utils; pub mod stake; pub mod test_utils; diff --git a/cli/src/offline/mod.rs b/cli/src/offline/mod.rs deleted file mode 100644 index 389eff5cc5..0000000000 --- a/cli/src/offline/mod.rs +++ /dev/null @@ -1,172 +0,0 @@ -use serde_json::Value; -use solana_clap_utils::keypair::presigner_from_pubkey_sigs; -use solana_cli_output::{CliSignOnlyData, OutputFormat}; -use solana_sdk::{ - hash::Hash, - pubkey::Pubkey, - signature::{Presigner, Signature}, - transaction::Transaction, -}; -use std::str::FromStr; - -pub fn return_signers( - tx: &Transaction, - output_format: &OutputFormat, -) -> Result> { - let verify_results = tx.verify_with_results(); - let mut signers = Vec::new(); - let mut absent = Vec::new(); - let mut bad_sig = Vec::new(); - tx.signatures - .iter() - .zip(tx.message.account_keys.iter()) - .zip(verify_results.into_iter()) - .for_each(|((sig, key), res)| { - if res { - signers.push(format!("{}={}", key, sig)) - } else if *sig == Signature::default() { - absent.push(key.to_string()); - } else { - bad_sig.push(key.to_string()); - } - }); - - let cli_command = CliSignOnlyData { - blockhash: tx.message.recent_blockhash.to_string(), - signers, - absent, - bad_sig, - }; - - Ok(output_format.formatted_string(&cli_command)) -} - -pub struct SignOnly { - pub blockhash: Hash, - pub present_signers: Vec<(Pubkey, Signature)>, - pub absent_signers: Vec, - pub bad_signers: Vec, -} - -impl SignOnly { - pub fn has_all_signers(&self) -> bool { - self.absent_signers.is_empty() && self.bad_signers.is_empty() - } - - pub fn presigner_of(&self, pubkey: &Pubkey) -> Option { - presigner_from_pubkey_sigs(pubkey, &self.present_signers) - } -} - -pub fn parse_sign_only_reply_string(reply: &str) -> SignOnly { - let object: Value = serde_json::from_str(&reply).unwrap(); - let blockhash_str = object.get("blockhash").unwrap().as_str().unwrap(); - let blockhash = blockhash_str.parse::().unwrap(); - let mut present_signers: Vec<(Pubkey, Signature)> = Vec::new(); - let signer_strings = object.get("signers"); - if let Some(sig_strings) = signer_strings { - present_signers = sig_strings - .as_array() - .unwrap() - .iter() - .map(|signer_string| { - let mut signer = signer_string.as_str().unwrap().split('='); - let key = Pubkey::from_str(signer.next().unwrap()).unwrap(); - let sig = Signature::from_str(signer.next().unwrap()).unwrap(); - (key, sig) - }) - .collect(); - } - let mut absent_signers: Vec = Vec::new(); - let signer_strings = object.get("absent"); - if let Some(sig_strings) = signer_strings { - absent_signers = sig_strings - .as_array() - .unwrap() - .iter() - .map(|val| { - let s = val.as_str().unwrap(); - Pubkey::from_str(s).unwrap() - }) - .collect(); - } - let mut bad_signers: Vec = Vec::new(); - let signer_strings = object.get("badSig"); - if let Some(sig_strings) = signer_strings { - bad_signers = sig_strings - .as_array() - .unwrap() - .iter() - .map(|val| { - let s = val.as_str().unwrap(); - Pubkey::from_str(s).unwrap() - }) - .collect(); - } - SignOnly { - blockhash, - present_signers, - absent_signers, - bad_signers, - } -} - -#[cfg(test)] -mod tests { - use super::*; - use solana_sdk::{ - message::Message, - pubkey::Pubkey, - signature::{keypair_from_seed, NullSigner, Signature, Signer, SignerError}, - system_instruction, - transaction::Transaction, - }; - - #[test] - fn test_return_signers() { - struct BadSigner { - pubkey: Pubkey, - } - - impl BadSigner { - pub fn new(pubkey: Pubkey) -> Self { - Self { pubkey } - } - } - - impl Signer for BadSigner { - fn try_pubkey(&self) -> Result { - Ok(self.pubkey) - } - - fn try_sign_message(&self, _message: &[u8]) -> Result { - Ok(Signature::new(&[1u8; 64])) - } - } - - let present: Box = Box::new(keypair_from_seed(&[2u8; 32]).unwrap()); - let absent: Box = Box::new(NullSigner::new(&Pubkey::new(&[3u8; 32]))); - let bad: Box = Box::new(BadSigner::new(Pubkey::new(&[4u8; 32]))); - let to = Pubkey::new(&[5u8; 32]); - let nonce = Pubkey::new(&[6u8; 32]); - let from = present.pubkey(); - let fee_payer = absent.pubkey(); - let nonce_auth = bad.pubkey(); - let mut tx = Transaction::new_unsigned(Message::new_with_nonce( - vec![system_instruction::transfer(&from, &to, 42)], - Some(&fee_payer), - &nonce, - &nonce_auth, - )); - - let signers = vec![present.as_ref(), absent.as_ref(), bad.as_ref()]; - let blockhash = Hash::new(&[7u8; 32]); - tx.try_partial_sign(&signers, blockhash).unwrap(); - let res = return_signers(&tx, &OutputFormat::JsonCompact).unwrap(); - let sign_only = parse_sign_only_reply_string(&res); - assert_eq!(sign_only.blockhash, blockhash); - assert_eq!(sign_only.present_signers[0].0, present.pubkey()); - assert_eq!(sign_only.absent_signers[0], absent.pubkey()); - assert_eq!(sign_only.bad_signers[0], bad.pubkey()); - } -} diff --git a/cli/src/stake.rs b/cli/src/stake.rs index 5b062fa5ce..e39474133b 100644 --- a/cli/src/stake.rs +++ b/cli/src/stake.rs @@ -5,7 +5,6 @@ use crate::{ ProcessResult, }, nonce::check_nonce_account, - offline::return_signers, spend_utils::{resolve_spend_tx_and_check_account_balances, SpendAmount}, }; use clap::{App, Arg, ArgGroup, ArgMatches, SubCommand}; @@ -18,7 +17,9 @@ use solana_clap_utils::{ offline::*, ArgConstant, }; -use solana_cli_output::{CliStakeHistory, CliStakeHistoryEntry, CliStakeState, CliStakeType}; +use solana_cli_output::{ + return_signers, CliStakeHistory, CliStakeHistoryEntry, CliStakeState, CliStakeType, +}; use solana_client::{ blockhash_query::BlockhashQuery, nonce_utils, rpc_client::RpcClient, rpc_request::DELINQUENT_VALIDATOR_SLOT_DISTANCE, diff --git a/cli/tests/nonce.rs b/cli/tests/nonce.rs index c9ccef83ac..06df183de1 100644 --- a/cli/tests/nonce.rs +++ b/cli/tests/nonce.rs @@ -1,10 +1,9 @@ use solana_cli::{ cli::{process_command, request_and_confirm_airdrop, CliCommand, CliConfig}, - offline::parse_sign_only_reply_string, spend_utils::SpendAmount, test_utils::{check_ready, check_recent_balance}, }; -use solana_cli_output::OutputFormat; +use solana_cli_output::{parse_sign_only_reply_string, OutputFormat}; use solana_client::{ blockhash_query::{self, BlockhashQuery}, nonce_utils, diff --git a/cli/tests/stake.rs b/cli/tests/stake.rs index d6c7b0e09e..91c058098e 100644 --- a/cli/tests/stake.rs +++ b/cli/tests/stake.rs @@ -1,10 +1,9 @@ use solana_cli::{ cli::{process_command, request_and_confirm_airdrop, CliCommand, CliConfig}, - offline::parse_sign_only_reply_string, spend_utils::SpendAmount, test_utils::{check_ready, check_recent_balance}, }; -use solana_cli_output::OutputFormat; +use solana_cli_output::{parse_sign_only_reply_string, OutputFormat}; use solana_client::{ blockhash_query::{self, BlockhashQuery}, nonce_utils, diff --git a/cli/tests/transfer.rs b/cli/tests/transfer.rs index 76cca770c8..7cda0ab44e 100644 --- a/cli/tests/transfer.rs +++ b/cli/tests/transfer.rs @@ -1,10 +1,9 @@ use solana_cli::{ cli::{process_command, request_and_confirm_airdrop, CliCommand, CliConfig}, - offline::parse_sign_only_reply_string, spend_utils::SpendAmount, test_utils::{check_ready, check_recent_balance}, }; -use solana_cli_output::OutputFormat; +use solana_cli_output::{parse_sign_only_reply_string, OutputFormat}; use solana_client::{ blockhash_query::{self, BlockhashQuery}, nonce_utils,