Add offline signing support to CLI (#7104)
This commit is contained in:
parent
294662a1ce
commit
88cb0c6ae3
|
@ -3,8 +3,9 @@ use clap::ArgMatches;
|
|||
use solana_sdk::{
|
||||
native_token::sol_to_lamports,
|
||||
pubkey::Pubkey,
|
||||
signature::{read_keypair_file, Keypair, KeypairUtil},
|
||||
signature::{read_keypair_file, Keypair, KeypairUtil, Signature},
|
||||
};
|
||||
use std::str::FromStr;
|
||||
|
||||
// Return parsed values from matches at `name`
|
||||
pub fn values_of<T>(matches: &ArgMatches<'_>, name: &str) -> Option<Vec<T>>
|
||||
|
@ -50,6 +51,20 @@ pub fn pubkey_of(matches: &ArgMatches<'_>, name: &str) -> Option<Pubkey> {
|
|||
value_of(matches, name).or_else(|| keypair_of(matches, name).map(|keypair| keypair.pubkey()))
|
||||
}
|
||||
|
||||
// Return pubkey/signature pairs for a string of the form pubkey=signature
|
||||
pub fn pubkeys_sigs_of(matches: &ArgMatches<'_>, name: &str) -> Option<Vec<(Pubkey, Signature)>> {
|
||||
matches.values_of(name).map(|values| {
|
||||
values
|
||||
.map(|pubkey_signer_string| {
|
||||
let mut signer = pubkey_signer_string.split('=');
|
||||
let key = Pubkey::from_str(signer.next().unwrap()).unwrap();
|
||||
let sig = Signature::from_str(signer.next().unwrap()).unwrap();
|
||||
(key, sig)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn amount_of(matches: &ArgMatches<'_>, name: &str, unit: &str) -> Option<u64> {
|
||||
if matches.value_of(unit) == Some("lamports") {
|
||||
value_of(matches, name)
|
||||
|
@ -172,4 +187,25 @@ mod tests {
|
|||
|
||||
fs::remove_file(&outfile).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pubkeys_sigs_of() {
|
||||
let key1 = Pubkey::new_rand();
|
||||
let key2 = Pubkey::new_rand();
|
||||
let sig1 = Keypair::new().sign_message(&[0u8]);
|
||||
let sig2 = Keypair::new().sign_message(&[1u8]);
|
||||
let signer1 = format!("{}={}", key1, sig1);
|
||||
let signer2 = format!("{}={}", key2, sig2);
|
||||
let matches = app().clone().get_matches_from(vec![
|
||||
"test",
|
||||
"--multiple",
|
||||
&signer1,
|
||||
"--multiple",
|
||||
&signer2,
|
||||
]);
|
||||
assert_eq!(
|
||||
pubkeys_sigs_of(&matches, "multiple"),
|
||||
Some(vec![(key1, sig1), (key2, sig2)])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
use crate::keypair::ASK_KEYWORD;
|
||||
use solana_sdk::hash::Hash;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::signature::read_keypair_file;
|
||||
use solana_sdk::signature::{read_keypair_file, Signature};
|
||||
use std::str::FromStr;
|
||||
|
||||
// Return an error if a pubkey cannot be parsed.
|
||||
pub fn is_pubkey(string: String) -> Result<(), String> {
|
||||
|
@ -10,6 +12,14 @@ pub fn is_pubkey(string: String) -> Result<(), String> {
|
|||
}
|
||||
}
|
||||
|
||||
// Return an error if a hash cannot be parsed.
|
||||
pub fn is_hash(string: String) -> Result<(), String> {
|
||||
match string.parse::<Hash>() {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(format!("{:?}", err)),
|
||||
}
|
||||
}
|
||||
|
||||
// Return an error if a keypair file cannot be parsed.
|
||||
pub fn is_keypair(string: String) -> Result<(), String> {
|
||||
read_keypair_file(&string)
|
||||
|
@ -32,6 +42,28 @@ pub fn is_pubkey_or_keypair(string: String) -> Result<(), String> {
|
|||
is_pubkey(string.clone()).or_else(|_| is_keypair(string))
|
||||
}
|
||||
|
||||
// Return an error if string cannot be parsed as pubkey=signature string
|
||||
pub fn is_pubkey_sig(string: String) -> Result<(), String> {
|
||||
let mut signer = string.split('=');
|
||||
match Pubkey::from_str(
|
||||
signer
|
||||
.next()
|
||||
.ok_or_else(|| "Malformed signer string".to_string())?,
|
||||
) {
|
||||
Ok(_) => {
|
||||
match Signature::from_str(
|
||||
signer
|
||||
.next()
|
||||
.ok_or_else(|| "Malformed signer string".to_string())?,
|
||||
) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(format!("{:?}", err)),
|
||||
}
|
||||
}
|
||||
Err(err) => Err(format!("{:?}", err)),
|
||||
}
|
||||
}
|
||||
|
||||
// Return an error if a url cannot be parsed.
|
||||
pub fn is_url(string: String) -> Result<(), String> {
|
||||
match url::Url::parse(&string) {
|
||||
|
|
338
cli/src/cli.rs
338
cli/src/cli.rs
|
@ -1,5 +1,10 @@
|
|||
use crate::{
|
||||
cluster_query::*, display::println_name_value, stake::*, storage::*, validator_info::*, vote::*,
|
||||
cluster_query::*,
|
||||
display::{println_name_value, println_signers},
|
||||
stake::*,
|
||||
storage::*,
|
||||
validator_info::*,
|
||||
vote::*,
|
||||
};
|
||||
use chrono::prelude::*;
|
||||
use clap::{App, AppSettings, Arg, ArgMatches, SubCommand};
|
||||
|
@ -105,8 +110,20 @@ pub enum CliCommand {
|
|||
lockup: Lockup,
|
||||
lamports: u64,
|
||||
},
|
||||
DeactivateStake(Pubkey),
|
||||
DelegateStake(Pubkey, Pubkey, bool),
|
||||
DeactivateStake {
|
||||
stake_account_pubkey: Pubkey,
|
||||
sign_only: bool,
|
||||
signers: Option<Vec<(Pubkey, Signature)>>,
|
||||
blockhash: Option<Hash>,
|
||||
},
|
||||
DelegateStake {
|
||||
stake_account_pubkey: Pubkey,
|
||||
vote_account_pubkey: Pubkey,
|
||||
force: bool,
|
||||
sign_only: bool,
|
||||
signers: Option<Vec<(Pubkey, Signature)>>,
|
||||
blockhash: Option<Hash>,
|
||||
},
|
||||
RedeemVoteCredits(Pubkey, Pubkey),
|
||||
ShowStakeHistory {
|
||||
use_lamports_unit: bool,
|
||||
|
@ -174,6 +191,9 @@ pub enum CliCommand {
|
|||
timestamp_pubkey: Option<Pubkey>,
|
||||
witnesses: Option<Vec<Pubkey>>,
|
||||
cancelable: bool,
|
||||
sign_only: bool,
|
||||
signers: Option<Vec<(Pubkey, Signature)>>,
|
||||
blockhash: Option<Hash>,
|
||||
},
|
||||
ShowAccount {
|
||||
pubkey: Pubkey,
|
||||
|
@ -413,6 +433,9 @@ pub fn parse_command(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, Box<dyn
|
|||
let timestamp_pubkey = value_of(&matches, "timestamp_pubkey");
|
||||
let witnesses = values_of(&matches, "witness");
|
||||
let cancelable = matches.is_present("cancelable");
|
||||
let sign_only = matches.is_present("sign_only");
|
||||
let signers = pubkeys_sigs_of(&matches, "signer");
|
||||
let blockhash = value_of(&matches, "blockhash");
|
||||
|
||||
Ok(CliCommandInfo {
|
||||
command: CliCommand::Pay {
|
||||
|
@ -422,8 +445,11 @@ pub fn parse_command(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, Box<dyn
|
|||
timestamp_pubkey,
|
||||
witnesses,
|
||||
cancelable,
|
||||
sign_only,
|
||||
signers,
|
||||
blockhash,
|
||||
},
|
||||
require_keypair: true,
|
||||
require_keypair: !sign_only,
|
||||
})
|
||||
}
|
||||
("show-account", Some(matches)) => {
|
||||
|
@ -522,6 +548,48 @@ pub fn check_unique_pubkeys(
|
|||
}
|
||||
}
|
||||
|
||||
pub fn get_blockhash_fee_calculator(
|
||||
rpc_client: &RpcClient,
|
||||
sign_only: bool,
|
||||
blockhash: Option<Hash>,
|
||||
) -> Result<(Hash, FeeCalculator), Box<dyn std::error::Error>> {
|
||||
Ok(if let Some(blockhash) = blockhash {
|
||||
if sign_only {
|
||||
(blockhash, FeeCalculator::default())
|
||||
} else {
|
||||
(blockhash, rpc_client.get_recent_blockhash()?.1)
|
||||
}
|
||||
} else {
|
||||
rpc_client.get_recent_blockhash()?
|
||||
})
|
||||
}
|
||||
|
||||
pub fn return_signers(tx: &Transaction) -> ProcessResult {
|
||||
println_signers(tx);
|
||||
let signers: Vec<_> = tx
|
||||
.signatures
|
||||
.iter()
|
||||
.zip(tx.message.account_keys.clone())
|
||||
.map(|(signature, pubkey)| format!("{}={}", pubkey, signature))
|
||||
.collect();
|
||||
|
||||
Ok(json!({
|
||||
"blockhash": tx.message.recent_blockhash.to_string(),
|
||||
"signers": &signers,
|
||||
})
|
||||
.to_string())
|
||||
}
|
||||
|
||||
pub fn replace_signatures(tx: &mut Transaction, signers: &[(Pubkey, Signature)]) -> ProcessResult {
|
||||
tx.replace_signatures(signers).map_err(|_| {
|
||||
CliError::BadParameter(
|
||||
"Transaction construction failed, incorrect signature or public key provided"
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
fn process_airdrop(
|
||||
rpc_client: &RpcClient,
|
||||
config: &CliConfig,
|
||||
|
@ -694,6 +762,7 @@ fn process_deploy(
|
|||
.to_string())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn process_pay(
|
||||
rpc_client: &RpcClient,
|
||||
config: &CliConfig,
|
||||
|
@ -703,12 +772,17 @@ fn process_pay(
|
|||
timestamp_pubkey: Option<Pubkey>,
|
||||
witnesses: &Option<Vec<Pubkey>>,
|
||||
cancelable: bool,
|
||||
sign_only: bool,
|
||||
signers: &Option<Vec<(Pubkey, Signature)>>,
|
||||
blockhash: Option<Hash>,
|
||||
) -> ProcessResult {
|
||||
check_unique_pubkeys(
|
||||
(&config.keypair.pubkey(), "cli keypair".to_string()),
|
||||
(to, "to".to_string()),
|
||||
)?;
|
||||
let (blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?;
|
||||
|
||||
let (blockhash, fee_calculator) =
|
||||
get_blockhash_fee_calculator(rpc_client, sign_only, blockhash)?;
|
||||
|
||||
let cancelable = if cancelable {
|
||||
Some(config.keypair.pubkey())
|
||||
|
@ -718,9 +792,17 @@ fn process_pay(
|
|||
|
||||
if timestamp == None && *witnesses == None {
|
||||
let mut tx = system_transaction::transfer(&config.keypair, to, lamports, blockhash);
|
||||
if let Some(signers) = signers {
|
||||
replace_signatures(&mut tx, &signers)?;
|
||||
}
|
||||
|
||||
if sign_only {
|
||||
return_signers(&tx)
|
||||
} else {
|
||||
check_account_for_fee(rpc_client, config, &fee_calculator, &tx.message)?;
|
||||
let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair]);
|
||||
log_instruction_custom_error::<SystemError>(result)
|
||||
}
|
||||
} else if *witnesses == None {
|
||||
let dt = timestamp.unwrap();
|
||||
let dt_pubkey = match timestamp_pubkey {
|
||||
|
@ -745,9 +827,15 @@ fn process_pay(
|
|||
ixs,
|
||||
blockhash,
|
||||
);
|
||||
if let Some(signers) = signers {
|
||||
replace_signatures(&mut tx, &signers)?;
|
||||
}
|
||||
if sign_only {
|
||||
return_signers(&tx)
|
||||
} else {
|
||||
check_account_for_fee(rpc_client, config, &fee_calculator, &tx.message)?;
|
||||
let result =
|
||||
rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair, &contract_state]);
|
||||
let result = rpc_client
|
||||
.send_and_confirm_transaction(&mut tx, &[&config.keypair, &contract_state]);
|
||||
let signature_str = log_instruction_custom_error::<BudgetError>(result)?;
|
||||
|
||||
Ok(json!({
|
||||
|
@ -755,9 +843,8 @@ fn process_pay(
|
|||
"processId": format!("{}", contract_state.pubkey()),
|
||||
})
|
||||
.to_string())
|
||||
}
|
||||
} else if timestamp == None {
|
||||
let (blockhash, _fee_calculator) = rpc_client.get_recent_blockhash()?;
|
||||
|
||||
let witness = if let Some(ref witness_vec) = *witnesses {
|
||||
witness_vec[0]
|
||||
} else {
|
||||
|
@ -783,8 +870,14 @@ fn process_pay(
|
|||
ixs,
|
||||
blockhash,
|
||||
);
|
||||
let result =
|
||||
rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair, &contract_state]);
|
||||
if let Some(signers) = signers {
|
||||
replace_signatures(&mut tx, &signers)?;
|
||||
}
|
||||
if sign_only {
|
||||
return_signers(&tx)
|
||||
} else {
|
||||
let result = rpc_client
|
||||
.send_and_confirm_transaction(&mut tx, &[&config.keypair, &contract_state]);
|
||||
check_account_for_fee(rpc_client, config, &fee_calculator, &tx.message)?;
|
||||
let signature_str = log_instruction_custom_error::<BudgetError>(result)?;
|
||||
|
||||
|
@ -793,6 +886,7 @@ fn process_pay(
|
|||
"processId": format!("{}", contract_state.pubkey()),
|
||||
})
|
||||
.to_string())
|
||||
}
|
||||
} else {
|
||||
Ok("Combo transactions not yet handled".to_string())
|
||||
}
|
||||
|
@ -926,18 +1020,36 @@ pub fn process_command(config: &CliConfig) -> ProcessResult {
|
|||
*lamports,
|
||||
),
|
||||
// Deactivate stake account
|
||||
CliCommand::DeactivateStake(stake_account_pubkey) => {
|
||||
process_deactivate_stake_account(&rpc_client, config, &stake_account_pubkey)
|
||||
}
|
||||
CliCommand::DelegateStake(stake_account_pubkey, vote_account_pubkey, force) => {
|
||||
process_delegate_stake(
|
||||
CliCommand::DeactivateStake {
|
||||
stake_account_pubkey,
|
||||
sign_only,
|
||||
ref signers,
|
||||
blockhash,
|
||||
} => process_deactivate_stake_account(
|
||||
&rpc_client,
|
||||
config,
|
||||
&stake_account_pubkey,
|
||||
*sign_only,
|
||||
signers,
|
||||
*blockhash,
|
||||
),
|
||||
CliCommand::DelegateStake {
|
||||
stake_account_pubkey,
|
||||
vote_account_pubkey,
|
||||
force,
|
||||
sign_only,
|
||||
ref signers,
|
||||
blockhash,
|
||||
} => process_delegate_stake(
|
||||
&rpc_client,
|
||||
config,
|
||||
&stake_account_pubkey,
|
||||
&vote_account_pubkey,
|
||||
*force,
|
||||
)
|
||||
}
|
||||
*sign_only,
|
||||
signers,
|
||||
*blockhash,
|
||||
),
|
||||
CliCommand::RedeemVoteCredits(stake_account_pubkey, vote_account_pubkey) => {
|
||||
process_redeem_vote_credits(
|
||||
&rpc_client,
|
||||
|
@ -1118,6 +1230,9 @@ pub fn process_command(config: &CliConfig) -> ProcessResult {
|
|||
timestamp_pubkey,
|
||||
ref witnesses,
|
||||
cancelable,
|
||||
sign_only,
|
||||
ref signers,
|
||||
blockhash,
|
||||
} => process_pay(
|
||||
&rpc_client,
|
||||
config,
|
||||
|
@ -1127,6 +1242,9 @@ pub fn process_command(config: &CliConfig) -> ProcessResult {
|
|||
*timestamp_pubkey,
|
||||
witnesses,
|
||||
*cancelable,
|
||||
*sign_only,
|
||||
signers,
|
||||
*blockhash,
|
||||
),
|
||||
CliCommand::ShowAccount {
|
||||
pubkey,
|
||||
|
@ -1410,6 +1528,29 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, '
|
|||
Arg::with_name("cancelable")
|
||||
.long("cancelable")
|
||||
.takes_value(false),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("sign_only")
|
||||
.long("sign-only")
|
||||
.takes_value(false)
|
||||
.help("Sign the transaction offline"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("signer")
|
||||
.long("signer")
|
||||
.value_name("PUBKEY=BASE58_SIG")
|
||||
.takes_value(true)
|
||||
.validator(is_pubkey_sig)
|
||||
.multiple(true)
|
||||
.help("Provide a public-key/signature pair for the transaction"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("blockhash")
|
||||
.long("blockhash")
|
||||
.value_name("BLOCKHASH")
|
||||
.takes_value(true)
|
||||
.validator(is_hash)
|
||||
.help("Use the supplied blockhash"),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
|
@ -1667,6 +1808,9 @@ mod tests {
|
|||
timestamp_pubkey: None,
|
||||
witnesses: None,
|
||||
cancelable: false,
|
||||
sign_only: false,
|
||||
signers: None,
|
||||
blockhash: None,
|
||||
},
|
||||
require_keypair: true
|
||||
}
|
||||
|
@ -1694,6 +1838,9 @@ mod tests {
|
|||
timestamp_pubkey: None,
|
||||
witnesses: Some(vec![witness0, witness1]),
|
||||
cancelable: false,
|
||||
sign_only: false,
|
||||
signers: None,
|
||||
blockhash: None,
|
||||
},
|
||||
require_keypair: true
|
||||
}
|
||||
|
@ -1717,6 +1864,9 @@ mod tests {
|
|||
timestamp_pubkey: None,
|
||||
witnesses: Some(vec![witness0]),
|
||||
cancelable: false,
|
||||
sign_only: false,
|
||||
signers: None,
|
||||
blockhash: None,
|
||||
},
|
||||
require_keypair: true
|
||||
}
|
||||
|
@ -1744,6 +1894,130 @@ mod tests {
|
|||
timestamp_pubkey: Some(witness0),
|
||||
witnesses: None,
|
||||
cancelable: false,
|
||||
sign_only: false,
|
||||
signers: None,
|
||||
blockhash: None,
|
||||
},
|
||||
require_keypair: true
|
||||
}
|
||||
);
|
||||
|
||||
// Test Pay Subcommand w/ sign-only
|
||||
let test_pay = test_commands.clone().get_matches_from(vec![
|
||||
"test",
|
||||
"pay",
|
||||
&pubkey_string,
|
||||
"50",
|
||||
"lamports",
|
||||
"--sign-only",
|
||||
]);
|
||||
assert_eq!(
|
||||
parse_command(&test_pay).unwrap(),
|
||||
CliCommandInfo {
|
||||
command: CliCommand::Pay {
|
||||
lamports: 50,
|
||||
to: pubkey,
|
||||
timestamp: None,
|
||||
timestamp_pubkey: None,
|
||||
witnesses: None,
|
||||
cancelable: false,
|
||||
sign_only: true,
|
||||
signers: None,
|
||||
blockhash: None,
|
||||
},
|
||||
require_keypair: false
|
||||
}
|
||||
);
|
||||
|
||||
// Test Pay Subcommand w/ signer
|
||||
let key1 = Pubkey::new_rand();
|
||||
let sig1 = Keypair::new().sign_message(&[0u8]);
|
||||
let signer1 = format!("{}={}", key1, sig1);
|
||||
let test_pay = test_commands.clone().get_matches_from(vec![
|
||||
"test",
|
||||
"pay",
|
||||
&pubkey_string,
|
||||
"50",
|
||||
"lamports",
|
||||
"--signer",
|
||||
&signer1,
|
||||
]);
|
||||
assert_eq!(
|
||||
parse_command(&test_pay).unwrap(),
|
||||
CliCommandInfo {
|
||||
command: CliCommand::Pay {
|
||||
lamports: 50,
|
||||
to: pubkey,
|
||||
timestamp: None,
|
||||
timestamp_pubkey: None,
|
||||
witnesses: None,
|
||||
cancelable: false,
|
||||
sign_only: false,
|
||||
signers: Some(vec![(key1, sig1)]),
|
||||
blockhash: None,
|
||||
},
|
||||
require_keypair: true
|
||||
}
|
||||
);
|
||||
|
||||
// Test Pay Subcommand w/ signers
|
||||
let key2 = Pubkey::new_rand();
|
||||
let sig2 = Keypair::new().sign_message(&[1u8]);
|
||||
let signer2 = format!("{}={}", key2, sig2);
|
||||
let test_pay = test_commands.clone().get_matches_from(vec![
|
||||
"test",
|
||||
"pay",
|
||||
&pubkey_string,
|
||||
"50",
|
||||
"lamports",
|
||||
"--signer",
|
||||
&signer1,
|
||||
"--signer",
|
||||
&signer2,
|
||||
]);
|
||||
assert_eq!(
|
||||
parse_command(&test_pay).unwrap(),
|
||||
CliCommandInfo {
|
||||
command: CliCommand::Pay {
|
||||
lamports: 50,
|
||||
to: pubkey,
|
||||
timestamp: None,
|
||||
timestamp_pubkey: None,
|
||||
witnesses: None,
|
||||
cancelable: false,
|
||||
sign_only: false,
|
||||
signers: Some(vec![(key1, sig1), (key2, sig2)]),
|
||||
blockhash: None,
|
||||
},
|
||||
require_keypair: true
|
||||
}
|
||||
);
|
||||
|
||||
// Test Pay Subcommand w/ Blockhash
|
||||
let blockhash = Hash::default();
|
||||
let blockhash_string = format!("{}", blockhash);
|
||||
let test_pay = test_commands.clone().get_matches_from(vec![
|
||||
"test",
|
||||
"pay",
|
||||
&pubkey_string,
|
||||
"50",
|
||||
"lamports",
|
||||
"--blockhash",
|
||||
&blockhash_string,
|
||||
]);
|
||||
assert_eq!(
|
||||
parse_command(&test_pay).unwrap(),
|
||||
CliCommandInfo {
|
||||
command: CliCommand::Pay {
|
||||
lamports: 50,
|
||||
to: pubkey,
|
||||
timestamp: None,
|
||||
timestamp_pubkey: None,
|
||||
witnesses: None,
|
||||
cancelable: false,
|
||||
sign_only: false,
|
||||
signers: None,
|
||||
blockhash: Some(blockhash),
|
||||
},
|
||||
require_keypair: true
|
||||
}
|
||||
|
@ -1788,6 +2062,9 @@ mod tests {
|
|||
timestamp_pubkey: Some(witness0),
|
||||
witnesses: Some(vec![witness0, witness1]),
|
||||
cancelable: false,
|
||||
sign_only: false,
|
||||
signers: None,
|
||||
blockhash: None,
|
||||
},
|
||||
require_keypair: true
|
||||
}
|
||||
|
@ -1894,7 +2171,12 @@ mod tests {
|
|||
assert_eq!(signature.unwrap(), SIGNATURE.to_string());
|
||||
|
||||
let stake_pubkey = Pubkey::new_rand();
|
||||
config.command = CliCommand::DeactivateStake(stake_pubkey);
|
||||
config.command = CliCommand::DeactivateStake {
|
||||
stake_account_pubkey: stake_pubkey,
|
||||
sign_only: false,
|
||||
signers: None,
|
||||
blockhash: None,
|
||||
};
|
||||
let signature = process_command(&config);
|
||||
assert_eq!(signature.unwrap(), SIGNATURE.to_string());
|
||||
|
||||
|
@ -1915,6 +2197,9 @@ mod tests {
|
|||
timestamp_pubkey: None,
|
||||
witnesses: None,
|
||||
cancelable: false,
|
||||
sign_only: false,
|
||||
signers: None,
|
||||
blockhash: None,
|
||||
};
|
||||
let signature = process_command(&config);
|
||||
assert_eq!(signature.unwrap(), SIGNATURE.to_string());
|
||||
|
@ -1928,6 +2213,9 @@ mod tests {
|
|||
timestamp_pubkey: Some(config.keypair.pubkey()),
|
||||
witnesses: None,
|
||||
cancelable: false,
|
||||
sign_only: false,
|
||||
signers: None,
|
||||
blockhash: None,
|
||||
};
|
||||
let result = process_command(&config);
|
||||
let json: Value = serde_json::from_str(&result.unwrap()).unwrap();
|
||||
|
@ -1949,6 +2237,9 @@ mod tests {
|
|||
timestamp_pubkey: None,
|
||||
witnesses: Some(vec![witness]),
|
||||
cancelable: true,
|
||||
sign_only: false,
|
||||
signers: None,
|
||||
blockhash: None,
|
||||
};
|
||||
let result = process_command(&config);
|
||||
let json: Value = serde_json::from_str(&result.unwrap()).unwrap();
|
||||
|
@ -2056,6 +2347,9 @@ mod tests {
|
|||
timestamp_pubkey: None,
|
||||
witnesses: None,
|
||||
cancelable: false,
|
||||
sign_only: false,
|
||||
signers: None,
|
||||
blockhash: None,
|
||||
};
|
||||
assert!(process_command(&config).is_err());
|
||||
|
||||
|
@ -2066,6 +2360,9 @@ mod tests {
|
|||
timestamp_pubkey: Some(config.keypair.pubkey()),
|
||||
witnesses: None,
|
||||
cancelable: false,
|
||||
sign_only: false,
|
||||
signers: None,
|
||||
blockhash: None,
|
||||
};
|
||||
assert!(process_command(&config).is_err());
|
||||
|
||||
|
@ -2076,6 +2373,9 @@ mod tests {
|
|||
timestamp_pubkey: None,
|
||||
witnesses: Some(vec![witness]),
|
||||
cancelable: true,
|
||||
sign_only: false,
|
||||
signers: None,
|
||||
blockhash: None,
|
||||
};
|
||||
assert!(process_command(&config).is_err());
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use console::style;
|
||||
use solana_sdk::transaction::Transaction;
|
||||
|
||||
// Pretty print a "name value"
|
||||
pub fn println_name_value(name: &str, value: &str) {
|
||||
|
@ -22,3 +23,14 @@ pub fn println_name_value_or(name: &str, value: &str, default_value: &str) {
|
|||
println!("{} {}", style(name).bold(), style(value));
|
||||
};
|
||||
}
|
||||
|
||||
pub fn println_signers(tx: &Transaction) {
|
||||
println!();
|
||||
println!("Blockhash: {}", tx.message.recent_blockhash);
|
||||
println!("Signers (Pubkey=Signature):");
|
||||
tx.signatures
|
||||
.iter()
|
||||
.zip(tx.message.account_keys.clone())
|
||||
.for_each(|(signature, pubkey)| println!(" {:?}={:?}", pubkey, signature));
|
||||
println!();
|
||||
}
|
||||
|
|
339
cli/src/stake.rs
339
cli/src/stake.rs
|
@ -1,14 +1,16 @@
|
|||
use crate::cli::{
|
||||
build_balance_message, check_account_for_fee, check_unique_pubkeys,
|
||||
log_instruction_custom_error, CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult,
|
||||
get_blockhash_fee_calculator, log_instruction_custom_error, replace_signatures, return_signers,
|
||||
CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult,
|
||||
};
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use console::style;
|
||||
use solana_clap_utils::{input_parsers::*, input_validators::*};
|
||||
use solana_client::rpc_client::RpcClient;
|
||||
use solana_sdk::signature::Keypair;
|
||||
use solana_sdk::signature::{Keypair, Signature};
|
||||
use solana_sdk::{
|
||||
account_utils::State,
|
||||
hash::Hash,
|
||||
pubkey::Pubkey,
|
||||
signature::KeypairUtil,
|
||||
system_instruction::SystemError,
|
||||
|
@ -120,6 +122,29 @@ impl StakeSubCommands for App<'_, '_> {
|
|||
.validator(is_pubkey_or_keypair)
|
||||
.help("The vote account to which the stake will be delegated")
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("sign_only")
|
||||
.long("sign-only")
|
||||
.takes_value(false)
|
||||
.help("Sign the transaction offline"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("signer")
|
||||
.long("signer")
|
||||
.value_name("PUBKEY=BASE58_SIG")
|
||||
.takes_value(true)
|
||||
.validator(is_pubkey_sig)
|
||||
.multiple(true)
|
||||
.help("Provide a public-key/signature pair for the transaction"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("blockhash")
|
||||
.long("blockhash")
|
||||
.value_name("BLOCKHASH")
|
||||
.takes_value(true)
|
||||
.validator(is_hash)
|
||||
.help("Use the supplied blockhash"),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("stake-authorize-staker")
|
||||
|
@ -176,6 +201,29 @@ impl StakeSubCommands for App<'_, '_> {
|
|||
.required(true)
|
||||
.help("Stake account to be deactivated.")
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("sign_only")
|
||||
.long("sign-only")
|
||||
.takes_value(false)
|
||||
.help("Sign the transaction offline"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("signer")
|
||||
.long("signer")
|
||||
.value_name("PUBKEY=BASE58_SIG")
|
||||
.takes_value(true)
|
||||
.validator(is_pubkey_sig)
|
||||
.multiple(true)
|
||||
.help("Provide a public-key/signature pair for the transaction"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("blockhash")
|
||||
.long("blockhash")
|
||||
.value_name("BLOCKHASH")
|
||||
.takes_value(true)
|
||||
.validator(is_hash)
|
||||
.help("Use the supplied blockhash"),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("withdraw-stake")
|
||||
|
@ -293,10 +341,20 @@ pub fn parse_stake_delegate_stake(matches: &ArgMatches<'_>) -> Result<CliCommand
|
|||
let stake_account_pubkey = pubkey_of(matches, "stake_account_pubkey").unwrap();
|
||||
let vote_account_pubkey = pubkey_of(matches, "vote_account_pubkey").unwrap();
|
||||
let force = matches.is_present("force");
|
||||
let sign_only = matches.is_present("sign_only");
|
||||
let signers = pubkeys_sigs_of(&matches, "signer");
|
||||
let blockhash = value_of(matches, "blockhash");
|
||||
|
||||
Ok(CliCommandInfo {
|
||||
command: CliCommand::DelegateStake(stake_account_pubkey, vote_account_pubkey, force),
|
||||
require_keypair: true,
|
||||
command: CliCommand::DelegateStake {
|
||||
stake_account_pubkey,
|
||||
vote_account_pubkey,
|
||||
force,
|
||||
sign_only,
|
||||
signers,
|
||||
blockhash,
|
||||
},
|
||||
require_keypair: !sign_only,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -328,9 +386,17 @@ pub fn parse_redeem_vote_credits(matches: &ArgMatches<'_>) -> Result<CliCommandI
|
|||
|
||||
pub fn parse_stake_deactivate_stake(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliError> {
|
||||
let stake_account_pubkey = pubkey_of(matches, "stake_account_pubkey").unwrap();
|
||||
let sign_only = matches.is_present("sign_only");
|
||||
let signers = pubkeys_sigs_of(&matches, "signer");
|
||||
let blockhash = value_of(matches, "blockhash");
|
||||
Ok(CliCommandInfo {
|
||||
command: CliCommand::DeactivateStake(stake_account_pubkey),
|
||||
require_keypair: true,
|
||||
command: CliCommand::DeactivateStake {
|
||||
stake_account_pubkey,
|
||||
sign_only,
|
||||
signers,
|
||||
blockhash,
|
||||
},
|
||||
require_keypair: !sign_only,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -463,8 +529,12 @@ pub fn process_deactivate_stake_account(
|
|||
rpc_client: &RpcClient,
|
||||
config: &CliConfig,
|
||||
stake_account_pubkey: &Pubkey,
|
||||
sign_only: bool,
|
||||
signers: &Option<Vec<(Pubkey, Signature)>>,
|
||||
blockhash: Option<Hash>,
|
||||
) -> ProcessResult {
|
||||
let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?;
|
||||
let (recent_blockhash, fee_calculator) =
|
||||
get_blockhash_fee_calculator(rpc_client, sign_only, blockhash)?;
|
||||
let ixs = vec![stake_instruction::deactivate_stake(
|
||||
stake_account_pubkey,
|
||||
&config.keypair.pubkey(),
|
||||
|
@ -475,9 +545,16 @@ pub fn process_deactivate_stake_account(
|
|||
&[&config.keypair],
|
||||
recent_blockhash,
|
||||
);
|
||||
if let Some(signers) = signers {
|
||||
replace_signatures(&mut tx, &signers)?;
|
||||
}
|
||||
if sign_only {
|
||||
return_signers(&tx)
|
||||
} else {
|
||||
check_account_for_fee(rpc_client, config, &fee_calculator, &tx.message)?;
|
||||
let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair]);
|
||||
log_instruction_custom_error::<StakeError>(result)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_withdraw_stake(
|
||||
|
@ -644,6 +721,9 @@ pub fn process_delegate_stake(
|
|||
stake_account_pubkey: &Pubkey,
|
||||
vote_account_pubkey: &Pubkey,
|
||||
force: bool,
|
||||
sign_only: bool,
|
||||
signers: &Option<Vec<(Pubkey, Signature)>>,
|
||||
blockhash: Option<Hash>,
|
||||
) -> ProcessResult {
|
||||
check_unique_pubkeys(
|
||||
(&config.keypair.pubkey(), "cli keypair".to_string()),
|
||||
|
@ -690,7 +770,8 @@ pub fn process_delegate_stake(
|
|||
}
|
||||
}
|
||||
|
||||
let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?;
|
||||
let (recent_blockhash, fee_calculator) =
|
||||
get_blockhash_fee_calculator(rpc_client, sign_only, blockhash)?;
|
||||
|
||||
let ixs = vec![stake_instruction::delegate_stake(
|
||||
stake_account_pubkey,
|
||||
|
@ -704,9 +785,16 @@ pub fn process_delegate_stake(
|
|||
&[&config.keypair],
|
||||
recent_blockhash,
|
||||
);
|
||||
if let Some(signers) = signers {
|
||||
replace_signatures(&mut tx, &signers)?;
|
||||
}
|
||||
if sign_only {
|
||||
return_signers(&tx)
|
||||
} else {
|
||||
check_account_for_fee(rpc_client, config, &fee_calculator, &tx.message)?;
|
||||
let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair]);
|
||||
log_instruction_custom_error::<StakeError>(result)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -831,18 +919,25 @@ mod tests {
|
|||
);
|
||||
|
||||
// Test DelegateStake Subcommand
|
||||
let stake_pubkey = Pubkey::new_rand();
|
||||
let stake_pubkey_string = stake_pubkey.to_string();
|
||||
let vote_account_pubkey = Pubkey::new_rand();
|
||||
let vote_account_string = vote_account_pubkey.to_string();
|
||||
let test_delegate_stake = test_commands.clone().get_matches_from(vec![
|
||||
"test",
|
||||
"delegate-stake",
|
||||
&stake_pubkey_string,
|
||||
&stake_account_string,
|
||||
&vote_account_string,
|
||||
]);
|
||||
assert_eq!(
|
||||
parse_command(&test_delegate_stake).unwrap(),
|
||||
CliCommandInfo {
|
||||
command: CliCommand::DelegateStake(stake_pubkey, stake_account_pubkey, false),
|
||||
command: CliCommand::DelegateStake {
|
||||
stake_account_pubkey,
|
||||
vote_account_pubkey,
|
||||
force: false,
|
||||
sign_only: false,
|
||||
signers: None,
|
||||
blockhash: None
|
||||
},
|
||||
require_keypair: true
|
||||
}
|
||||
);
|
||||
|
@ -851,13 +946,124 @@ mod tests {
|
|||
"test",
|
||||
"delegate-stake",
|
||||
"--force",
|
||||
&stake_pubkey_string,
|
||||
&stake_account_string,
|
||||
&vote_account_string,
|
||||
]);
|
||||
assert_eq!(
|
||||
parse_command(&test_delegate_stake).unwrap(),
|
||||
CliCommandInfo {
|
||||
command: CliCommand::DelegateStake(stake_pubkey, stake_account_pubkey, true),
|
||||
command: CliCommand::DelegateStake {
|
||||
stake_account_pubkey,
|
||||
vote_account_pubkey,
|
||||
force: true,
|
||||
sign_only: false,
|
||||
signers: None,
|
||||
blockhash: None
|
||||
},
|
||||
require_keypair: true
|
||||
}
|
||||
);
|
||||
|
||||
// Test Delegate Subcommand w/ Blockhash
|
||||
let blockhash = Hash::default();
|
||||
let blockhash_string = format!("{}", blockhash);
|
||||
let test_delegate_stake = test_commands.clone().get_matches_from(vec![
|
||||
"test",
|
||||
"delegate-stake",
|
||||
&stake_account_string,
|
||||
&vote_account_string,
|
||||
"--blockhash",
|
||||
&blockhash_string,
|
||||
]);
|
||||
assert_eq!(
|
||||
parse_command(&test_delegate_stake).unwrap(),
|
||||
CliCommandInfo {
|
||||
command: CliCommand::DelegateStake {
|
||||
stake_account_pubkey,
|
||||
vote_account_pubkey,
|
||||
force: false,
|
||||
sign_only: false,
|
||||
signers: None,
|
||||
blockhash: Some(blockhash)
|
||||
},
|
||||
require_keypair: true
|
||||
}
|
||||
);
|
||||
|
||||
let test_delegate_stake = test_commands.clone().get_matches_from(vec![
|
||||
"test",
|
||||
"delegate-stake",
|
||||
&stake_account_string,
|
||||
&vote_account_string,
|
||||
"--sign-only",
|
||||
]);
|
||||
assert_eq!(
|
||||
parse_command(&test_delegate_stake).unwrap(),
|
||||
CliCommandInfo {
|
||||
command: CliCommand::DelegateStake {
|
||||
stake_account_pubkey,
|
||||
vote_account_pubkey,
|
||||
force: false,
|
||||
sign_only: true,
|
||||
signers: None,
|
||||
blockhash: None
|
||||
},
|
||||
require_keypair: false
|
||||
}
|
||||
);
|
||||
|
||||
// Test Delegate Subcommand w/ signer
|
||||
let key1 = Pubkey::new_rand();
|
||||
let sig1 = Keypair::new().sign_message(&[0u8]);
|
||||
let signer1 = format!("{}={}", key1, sig1);
|
||||
let test_delegate_stake = test_commands.clone().get_matches_from(vec![
|
||||
"test",
|
||||
"delegate-stake",
|
||||
&stake_account_string,
|
||||
&vote_account_string,
|
||||
"--signer",
|
||||
&signer1,
|
||||
]);
|
||||
assert_eq!(
|
||||
parse_command(&test_delegate_stake).unwrap(),
|
||||
CliCommandInfo {
|
||||
command: CliCommand::DelegateStake {
|
||||
stake_account_pubkey,
|
||||
vote_account_pubkey,
|
||||
force: false,
|
||||
sign_only: false,
|
||||
signers: Some(vec![(key1, sig1)]),
|
||||
blockhash: None
|
||||
},
|
||||
require_keypair: true
|
||||
}
|
||||
);
|
||||
|
||||
// Test Delegate Subcommand w/ signers
|
||||
let key2 = Pubkey::new_rand();
|
||||
let sig2 = Keypair::new().sign_message(&[0u8]);
|
||||
let signer2 = format!("{}={}", key2, sig2);
|
||||
let test_delegate_stake = test_commands.clone().get_matches_from(vec![
|
||||
"test",
|
||||
"delegate-stake",
|
||||
&stake_account_string,
|
||||
&vote_account_string,
|
||||
"--signer",
|
||||
&signer1,
|
||||
"--signer",
|
||||
&signer2,
|
||||
]);
|
||||
assert_eq!(
|
||||
parse_command(&test_delegate_stake).unwrap(),
|
||||
CliCommandInfo {
|
||||
command: CliCommand::DelegateStake {
|
||||
stake_account_pubkey,
|
||||
vote_account_pubkey,
|
||||
force: false,
|
||||
sign_only: false,
|
||||
signers: Some(vec![(key1, sig1), (key2, sig2)]),
|
||||
blockhash: None
|
||||
},
|
||||
require_keypair: true
|
||||
}
|
||||
);
|
||||
|
@ -866,7 +1072,7 @@ mod tests {
|
|||
let test_withdraw_stake = test_commands.clone().get_matches_from(vec![
|
||||
"test",
|
||||
"withdraw-stake",
|
||||
&stake_pubkey_string,
|
||||
&stake_account_string,
|
||||
&stake_account_string,
|
||||
"42",
|
||||
"lamports",
|
||||
|
@ -875,7 +1081,7 @@ mod tests {
|
|||
assert_eq!(
|
||||
parse_command(&test_withdraw_stake).unwrap(),
|
||||
CliCommandInfo {
|
||||
command: CliCommand::WithdrawStake(stake_pubkey, stake_account_pubkey, 42),
|
||||
command: CliCommand::WithdrawStake(stake_account_pubkey, stake_account_pubkey, 42),
|
||||
require_keypair: true
|
||||
}
|
||||
);
|
||||
|
@ -884,12 +1090,109 @@ mod tests {
|
|||
let test_deactivate_stake = test_commands.clone().get_matches_from(vec![
|
||||
"test",
|
||||
"deactivate-stake",
|
||||
&stake_pubkey_string,
|
||||
&stake_account_string,
|
||||
]);
|
||||
assert_eq!(
|
||||
parse_command(&test_deactivate_stake).unwrap(),
|
||||
CliCommandInfo {
|
||||
command: CliCommand::DeactivateStake(stake_pubkey),
|
||||
command: CliCommand::DeactivateStake {
|
||||
stake_account_pubkey,
|
||||
sign_only: false,
|
||||
signers: None,
|
||||
blockhash: None
|
||||
},
|
||||
require_keypair: true
|
||||
}
|
||||
);
|
||||
|
||||
// Test Deactivate Subcommand w/ Blockhash
|
||||
let blockhash = Hash::default();
|
||||
let blockhash_string = format!("{}", blockhash);
|
||||
let test_deactivate_stake = test_commands.clone().get_matches_from(vec![
|
||||
"test",
|
||||
"deactivate-stake",
|
||||
&stake_account_string,
|
||||
"--blockhash",
|
||||
&blockhash_string,
|
||||
]);
|
||||
assert_eq!(
|
||||
parse_command(&test_deactivate_stake).unwrap(),
|
||||
CliCommandInfo {
|
||||
command: CliCommand::DeactivateStake {
|
||||
stake_account_pubkey,
|
||||
sign_only: false,
|
||||
signers: None,
|
||||
blockhash: Some(blockhash)
|
||||
},
|
||||
require_keypair: true
|
||||
}
|
||||
);
|
||||
|
||||
let test_deactivate_stake = test_commands.clone().get_matches_from(vec![
|
||||
"test",
|
||||
"deactivate-stake",
|
||||
&stake_account_string,
|
||||
"--sign-only",
|
||||
]);
|
||||
assert_eq!(
|
||||
parse_command(&test_deactivate_stake).unwrap(),
|
||||
CliCommandInfo {
|
||||
command: CliCommand::DeactivateStake {
|
||||
stake_account_pubkey,
|
||||
sign_only: true,
|
||||
signers: None,
|
||||
blockhash: None
|
||||
},
|
||||
require_keypair: false
|
||||
}
|
||||
);
|
||||
|
||||
// Test Deactivate Subcommand w/ signers
|
||||
let key1 = Pubkey::new_rand();
|
||||
let sig1 = Keypair::new().sign_message(&[0u8]);
|
||||
let signer1 = format!("{}={}", key1, sig1);
|
||||
let test_deactivate_stake = test_commands.clone().get_matches_from(vec![
|
||||
"test",
|
||||
"deactivate-stake",
|
||||
&stake_account_string,
|
||||
"--signer",
|
||||
&signer1,
|
||||
]);
|
||||
assert_eq!(
|
||||
parse_command(&test_deactivate_stake).unwrap(),
|
||||
CliCommandInfo {
|
||||
command: CliCommand::DeactivateStake {
|
||||
stake_account_pubkey,
|
||||
sign_only: false,
|
||||
signers: Some(vec![(key1, sig1)]),
|
||||
blockhash: None
|
||||
},
|
||||
require_keypair: true
|
||||
}
|
||||
);
|
||||
|
||||
// Test Deactivate Subcommand w/ signers
|
||||
let key2 = Pubkey::new_rand();
|
||||
let sig2 = Keypair::new().sign_message(&[0u8]);
|
||||
let signer2 = format!("{}={}", key2, sig2);
|
||||
let test_deactivate_stake = test_commands.clone().get_matches_from(vec![
|
||||
"test",
|
||||
"deactivate-stake",
|
||||
&stake_account_string,
|
||||
"--signer",
|
||||
&signer1,
|
||||
"--signer",
|
||||
&signer2,
|
||||
]);
|
||||
assert_eq!(
|
||||
parse_command(&test_deactivate_stake).unwrap(),
|
||||
CliCommandInfo {
|
||||
command: CliCommand::DeactivateStake {
|
||||
stake_account_pubkey,
|
||||
sign_only: false,
|
||||
signers: Some(vec![(key1, sig1), (key2, sig2)]),
|
||||
blockhash: None
|
||||
},
|
||||
require_keypair: true
|
||||
}
|
||||
);
|
||||
|
|
104
cli/tests/pay.rs
104
cli/tests/pay.rs
|
@ -3,9 +3,9 @@ use serde_json::Value;
|
|||
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::pubkey::Pubkey;
|
||||
use solana_sdk::signature::KeypairUtil;
|
||||
use solana_sdk::{hash::Hash, pubkey::Pubkey, signature::KeypairUtil, signature::Signature};
|
||||
use std::fs::remove_dir_all;
|
||||
use std::str::FromStr;
|
||||
use std::sync::mpsc::channel;
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -71,6 +71,9 @@ fn test_cli_timestamp_tx() {
|
|||
timestamp_pubkey: Some(config_witness.keypair.pubkey()),
|
||||
witnesses: None,
|
||||
cancelable: false,
|
||||
sign_only: false,
|
||||
signers: None,
|
||||
blockhash: None,
|
||||
};
|
||||
let sig_response = process_command(&config_payer);
|
||||
|
||||
|
@ -138,6 +141,9 @@ fn test_cli_witness_tx() {
|
|||
timestamp_pubkey: None,
|
||||
witnesses: Some(vec![config_witness.keypair.pubkey()]),
|
||||
cancelable: false,
|
||||
sign_only: false,
|
||||
signers: None,
|
||||
blockhash: None,
|
||||
};
|
||||
let sig_response = process_command(&config_payer);
|
||||
|
||||
|
@ -198,6 +204,9 @@ fn test_cli_cancel_tx() {
|
|||
timestamp_pubkey: None,
|
||||
witnesses: Some(vec![config_witness.keypair.pubkey()]),
|
||||
cancelable: true,
|
||||
sign_only: false,
|
||||
signers: None,
|
||||
blockhash: None,
|
||||
};
|
||||
let sig_response = process_command(&config_payer).unwrap();
|
||||
|
||||
|
@ -223,3 +232,94 @@ fn test_cli_cancel_tx() {
|
|||
server.close().unwrap();
|
||||
remove_dir_all(ledger_path).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_offline_pay_tx() {
|
||||
let (server, leader_data, alice, ledger_path) = new_validator_for_tests();
|
||||
let bob_pubkey = Pubkey::new_rand();
|
||||
|
||||
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_offline = CliConfig::default();
|
||||
config_offline.json_rpc_url =
|
||||
format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port());
|
||||
let mut config_online = CliConfig::default();
|
||||
config_online.json_rpc_url =
|
||||
format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port());
|
||||
assert_ne!(
|
||||
config_offline.keypair.pubkey(),
|
||||
config_online.keypair.pubkey()
|
||||
);
|
||||
|
||||
request_and_confirm_airdrop(
|
||||
&rpc_client,
|
||||
&drone_addr,
|
||||
&config_offline.keypair.pubkey(),
|
||||
50,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
request_and_confirm_airdrop(
|
||||
&rpc_client,
|
||||
&drone_addr,
|
||||
&config_online.keypair.pubkey(),
|
||||
50,
|
||||
)
|
||||
.unwrap();
|
||||
check_balance(50, &rpc_client, &config_offline.keypair.pubkey());
|
||||
check_balance(50, &rpc_client, &config_online.keypair.pubkey());
|
||||
|
||||
config_offline.command = CliCommand::Pay {
|
||||
lamports: 10,
|
||||
to: bob_pubkey,
|
||||
timestamp: None,
|
||||
timestamp_pubkey: None,
|
||||
witnesses: None,
|
||||
cancelable: false,
|
||||
sign_only: true,
|
||||
signers: None,
|
||||
blockhash: None,
|
||||
};
|
||||
let sig_response = process_command(&config_offline).unwrap();
|
||||
|
||||
check_balance(50, &rpc_client, &config_offline.keypair.pubkey());
|
||||
check_balance(50, &rpc_client, &config_online.keypair.pubkey());
|
||||
check_balance(0, &rpc_client, &bob_pubkey);
|
||||
|
||||
let object: Value = serde_json::from_str(&sig_response).unwrap();
|
||||
let blockhash_str = object.get("blockhash").unwrap().as_str().unwrap();
|
||||
let signer_strings = object.get("signers").unwrap().as_array().unwrap();
|
||||
let signers: Vec<_> = signer_strings
|
||||
.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();
|
||||
|
||||
config_online.command = CliCommand::Pay {
|
||||
lamports: 10,
|
||||
to: bob_pubkey,
|
||||
timestamp: None,
|
||||
timestamp_pubkey: None,
|
||||
witnesses: None,
|
||||
cancelable: false,
|
||||
sign_only: false,
|
||||
signers: Some(signers),
|
||||
blockhash: Some(blockhash_str.parse::<Hash>().unwrap()),
|
||||
};
|
||||
process_command(&config_online).unwrap();
|
||||
|
||||
check_balance(40, &rpc_client, &config_offline.keypair.pubkey());
|
||||
check_balance(50, &rpc_client, &config_online.keypair.pubkey());
|
||||
check_balance(10, &rpc_client, &bob_pubkey);
|
||||
|
||||
server.close().unwrap();
|
||||
remove_dir_all(ledger_path).unwrap();
|
||||
}
|
||||
|
|
|
@ -117,7 +117,7 @@ impl Instruction {
|
|||
pub struct AccountMeta {
|
||||
/// An account's public key
|
||||
pub pubkey: Pubkey,
|
||||
/// True if an Instruciton requires a Transaction signature matching `pubkey`.
|
||||
/// True if an Instruction requires a Transaction signature matching `pubkey`.
|
||||
pub is_signer: bool,
|
||||
/// True if the `pubkey` can be loaded as a read-write account.
|
||||
pub is_writable: bool,
|
||||
|
|
|
@ -51,6 +51,9 @@ pub enum TransactionError {
|
|||
|
||||
/// Transaction contains an invalid account reference
|
||||
InvalidAccountIndex,
|
||||
|
||||
/// Transaction did not pass signature verification
|
||||
SignatureFailure,
|
||||
}
|
||||
|
||||
pub type Result<T> = result::Result<T, TransactionError>;
|
||||
|
@ -225,6 +228,21 @@ impl Transaction {
|
|||
}
|
||||
}
|
||||
|
||||
/// Verify the transaction
|
||||
pub fn verify(&self) -> Result<()> {
|
||||
if !self
|
||||
.signatures
|
||||
.iter()
|
||||
.zip(&self.message.account_keys)
|
||||
.map(|(signature, pubkey)| signature.verify(pubkey.as_ref(), &self.message_data()))
|
||||
.all(|verify_result| verify_result)
|
||||
{
|
||||
Err(TransactionError::SignatureFailure)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the positions of the pubkeys in `account_keys` associated with signing keypairs
|
||||
pub fn get_signing_keypair_positions<T: KeypairUtil>(
|
||||
&self,
|
||||
|
@ -246,6 +264,27 @@ impl Transaction {
|
|||
.collect())
|
||||
}
|
||||
|
||||
/// Replace all the signatures and pubkeys
|
||||
pub fn replace_signatures(&mut self, signers: &[(Pubkey, Signature)]) -> Result<()> {
|
||||
let num_required_signatures = self.message.header.num_required_signatures as usize;
|
||||
if signers.len() != num_required_signatures
|
||||
|| self.signatures.len() != num_required_signatures
|
||||
|| self.message.account_keys.len() < num_required_signatures
|
||||
{
|
||||
return Err(TransactionError::InvalidAccountIndex);
|
||||
}
|
||||
|
||||
signers
|
||||
.iter()
|
||||
.enumerate()
|
||||
.for_each(|(i, (pubkey, signature))| {
|
||||
self.signatures[i] = *signature;
|
||||
self.message.account_keys[i] = *pubkey;
|
||||
});
|
||||
|
||||
self.verify()
|
||||
}
|
||||
|
||||
pub fn is_signed(&self) -> bool {
|
||||
self.signatures
|
||||
.iter()
|
||||
|
|
Loading…
Reference in New Issue