From 44e45aa090747fb40dfddf3267bff5a7618c7990 Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Fri, 27 Dec 2019 14:35:49 -0600 Subject: [PATCH] Support nonced transactions in the CLI (#7624) * Support nonced transactions in the CLI * Update nonce.rs --- cli/src/cli.rs | 389 +++++++++++++++++--------- cli/src/nonce.rs | 125 ++++++++- cli/src/stake.rs | 176 ++++++++++-- cli/tests/pay.rs | 148 +++++++--- cli/tests/stake.rs | 119 +++++++- client/src/mock_rpc_client_request.rs | 15 +- client/src/rpc_client.rs | 8 +- client/src/rpc_request.rs | 2 +- sdk/src/system_transaction.rs | 22 ++ sdk/src/transaction.rs | 14 + 10 files changed, 808 insertions(+), 210 deletions(-) diff --git a/cli/src/cli.rs b/cli/src/cli.rs index c8d1f3fcc..71b8140b2 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -72,6 +72,21 @@ impl std::ops::Deref for KeypairEq { } } +#[derive(Default, Debug, PartialEq)] +pub struct PayCommand { + pub lamports: u64, + pub to: Pubkey, + pub timestamp: Option>, + pub timestamp_pubkey: Option, + pub witnesses: Option>, + pub cancelable: bool, + pub sign_only: bool, + pub signers: Option>, + pub blockhash: Option, + pub nonce_account: Option, + pub nonce_authority: Option, +} + #[derive(Debug, PartialEq)] #[allow(clippy::large_enum_variant)] pub enum CliCommand { @@ -155,6 +170,8 @@ pub enum CliCommand { sign_only: bool, signers: Option>, blockhash: Option, + nonce_account: Option, + nonce_authority: Option, }, DelegateStake { stake_account_pubkey: Pubkey, @@ -163,6 +180,8 @@ pub enum CliCommand { sign_only: bool, signers: Option>, blockhash: Option, + nonce_account: Option, + nonce_authority: Option, }, RedeemVoteCredits(Pubkey, Pubkey), ShowStakeHistory { @@ -233,17 +252,7 @@ pub enum CliCommand { }, Cancel(Pubkey), Confirm(Signature), - Pay { - lamports: u64, - to: Pubkey, - timestamp: Option>, - timestamp_pubkey: Option, - witnesses: Option>, - cancelable: bool, - sign_only: bool, - signers: Option>, - blockhash: Option, - }, + Pay(PayCommand), ShowAccount { pubkey: Pubkey, output_file: Option, @@ -259,11 +268,12 @@ pub struct CliCommandInfo { pub require_keypair: bool, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum CliError { BadParameter(String), CommandNotRecognized(String), InsufficientFundsForFee, + InvalidNonce(CliNonceError), DynamicProgramError(String), RpcRequestError(String), KeypairFileNotFound(String), @@ -497,9 +507,19 @@ pub fn parse_command(matches: &ArgMatches<'_>) -> Result) -> Result>, blockhash: Option, + nonce_account: Option, + nonce_authority: Option<&Keypair>, ) -> ProcessResult { check_unique_pubkeys( (&config.keypair.pubkey(), "cli keypair".to_string()), @@ -903,7 +927,20 @@ fn process_pay( }; if timestamp == None && *witnesses == None { - let mut tx = system_transaction::transfer(&config.keypair, to, lamports, blockhash); + let mut tx = if let Some(nonce_account) = &nonce_account { + let nonce_authority: &Keypair = nonce_authority.unwrap_or(&config.keypair); + system_transaction::nonced_transfer( + &config.keypair, + to, + lamports, + nonce_account, + nonce_authority, + blockhash, + ) + } else { + system_transaction::transfer(&config.keypair, to, lamports, blockhash) + }; + if let Some(signers) = signers { replace_signatures(&mut tx, &signers)?; } @@ -911,6 +948,11 @@ fn process_pay( if sign_only { return_signers(&tx) } else { + if let Some(nonce_account) = &nonce_account { + let nonce_authority: &Keypair = nonce_authority.unwrap_or(&config.keypair); + let nonce_account = rpc_client.get_account(nonce_account)?; + check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &blockhash)?; + } check_account_for_fee( rpc_client, &config.keypair.pubkey(), @@ -1229,6 +1271,8 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { sign_only, ref signers, blockhash, + nonce_account, + ref nonce_authority, } => process_deactivate_stake_account( &rpc_client, config, @@ -1236,6 +1280,8 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { *sign_only, signers, *blockhash, + *nonce_account, + nonce_authority.as_deref(), ), CliCommand::DelegateStake { stake_account_pubkey, @@ -1244,6 +1290,8 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { sign_only, ref signers, blockhash, + nonce_account, + ref nonce_authority, } => process_delegate_stake( &rpc_client, config, @@ -1253,6 +1301,8 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { *sign_only, signers, *blockhash, + *nonce_account, + nonce_authority.as_deref(), ), CliCommand::RedeemVoteCredits(stake_account_pubkey, vote_account_pubkey) => { process_redeem_vote_credits( @@ -1438,7 +1488,7 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { // Confirm the last client transaction by signature CliCommand::Confirm(signature) => process_confirm(&rpc_client, signature), // If client has positive balance, pay lamports to another address - CliCommand::Pay { + CliCommand::Pay(PayCommand { lamports, to, timestamp, @@ -1448,7 +1498,9 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { sign_only, ref signers, blockhash, - } => process_pay( + nonce_account, + ref nonce_authority, + }) => process_pay( &rpc_client, config, *lamports, @@ -1460,6 +1512,8 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { *sign_only, signers, *blockhash, + *nonce_account, + nonce_authority.as_deref(), ), CliCommand::ShowAccount { pubkey, @@ -1801,6 +1855,23 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' .takes_value(false) .help("Sign the transaction offline"), ) + .arg( + Arg::with_name(NONCE_ARG.name) + .long(NONCE_ARG.long) + .takes_value(true) + .value_name("PUBKEY") + .requires("blockhash") + .validator(is_pubkey_or_keypair) + .help(NONCE_ARG.help), + ) + .arg( + Arg::with_name(NONCE_AUTHORITY_ARG.name) + .long(NONCE_AUTHORITY_ARG.long) + .takes_value(true) + .requires(NONCE_ARG.name) + .validator(is_keypair_or_ask_keyword) + .help(NONCE_AUTHORITY_ARG.help), + ) .arg( Arg::with_name("signer") .long("signer") @@ -1903,12 +1974,18 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' mod tests { use super::*; use serde_json::Value; - use solana_client::mock_rpc_client_request::SIGNATURE; + use solana_client::{ + mock_rpc_client_request::SIGNATURE, + rpc_request::{self, RpcRequest, RpcResponseContext}, + }; use solana_sdk::{ + account::Account, + nonce_program, + nonce_state::{Meta as NonceMeta, NonceState}, signature::{read_keypair_file, write_keypair_file}, transaction::TransactionError, }; - use std::path::PathBuf; + use std::{collections::HashMap, path::PathBuf}; fn make_tmp_path(name: &str) -> String { let out_dir = std::env::var("FARF_DIR").unwrap_or_else(|_| "farf".to_string()); @@ -2106,17 +2183,11 @@ mod tests { assert_eq!( parse_command(&test_pay).unwrap(), CliCommandInfo { - command: CliCommand::Pay { + command: CliCommand::Pay(PayCommand { lamports: 50, to: pubkey, - timestamp: None, - timestamp_pubkey: None, - witnesses: None, - cancelable: false, - sign_only: false, - signers: None, - blockhash: None, - }, + ..PayCommand::default() + }), require_keypair: true } ); @@ -2136,17 +2207,12 @@ mod tests { assert_eq!( parse_command(&test_pay_multiple_witnesses).unwrap(), CliCommandInfo { - command: CliCommand::Pay { + command: CliCommand::Pay(PayCommand { lamports: 50, to: pubkey, - timestamp: None, - timestamp_pubkey: None, witnesses: Some(vec![witness0, witness1]), - cancelable: false, - sign_only: false, - signers: None, - blockhash: None, - }, + ..PayCommand::default() + }), require_keypair: true } ); @@ -2162,17 +2228,12 @@ mod tests { assert_eq!( parse_command(&test_pay_single_witness).unwrap(), CliCommandInfo { - command: CliCommand::Pay { + command: CliCommand::Pay(PayCommand { lamports: 50, to: pubkey, - timestamp: None, - timestamp_pubkey: None, witnesses: Some(vec![witness0]), - cancelable: false, - sign_only: false, - signers: None, - blockhash: None, - }, + ..PayCommand::default() + }), require_keypair: true } ); @@ -2192,17 +2253,13 @@ mod tests { assert_eq!( parse_command(&test_pay_timestamp).unwrap(), CliCommandInfo { - command: CliCommand::Pay { + command: CliCommand::Pay(PayCommand { lamports: 50, to: pubkey, timestamp: Some(dt), timestamp_pubkey: Some(witness0), - witnesses: None, - cancelable: false, - sign_only: false, - signers: None, - blockhash: None, - }, + ..PayCommand::default() + }), require_keypair: true } ); @@ -2219,17 +2276,12 @@ mod tests { assert_eq!( parse_command(&test_pay).unwrap(), CliCommandInfo { - command: CliCommand::Pay { + command: CliCommand::Pay(PayCommand { lamports: 50, to: pubkey, - timestamp: None, - timestamp_pubkey: None, - witnesses: None, - cancelable: false, sign_only: true, - signers: None, - blockhash: None, - }, + ..PayCommand::default() + }), require_keypair: true, } ); @@ -2250,17 +2302,12 @@ mod tests { assert_eq!( parse_command(&test_pay).unwrap(), CliCommandInfo { - command: CliCommand::Pay { + command: CliCommand::Pay(PayCommand { lamports: 50, to: pubkey, - timestamp: None, - timestamp_pubkey: None, - witnesses: None, - cancelable: false, - sign_only: false, signers: Some(vec![(key1, sig1)]), - blockhash: None, - }, + ..PayCommand::default() + }), require_keypair: true } ); @@ -2283,17 +2330,12 @@ mod tests { assert_eq!( parse_command(&test_pay).unwrap(), CliCommandInfo { - command: CliCommand::Pay { + command: CliCommand::Pay(PayCommand { 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, - }, + ..PayCommand::default() + }), require_keypair: true } ); @@ -2313,17 +2355,72 @@ mod tests { assert_eq!( parse_command(&test_pay).unwrap(), CliCommandInfo { - command: CliCommand::Pay { + command: CliCommand::Pay(PayCommand { lamports: 50, to: pubkey, - timestamp: None, - timestamp_pubkey: None, - witnesses: None, - cancelable: false, - sign_only: false, - signers: None, blockhash: Some(blockhash), - }, + ..PayCommand::default() + }), + require_keypair: true + } + ); + + // Test Pay Subcommand w/ Nonce + 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, + "--nonce", + &pubkey_string, + ]); + assert_eq!( + parse_command(&test_pay).unwrap(), + CliCommandInfo { + command: CliCommand::Pay(PayCommand { + lamports: 50, + to: pubkey, + blockhash: Some(blockhash), + nonce_account: Some(pubkey), + ..PayCommand::default() + }), + require_keypair: true + } + ); + + // Test Pay Subcommand w/ Nonce and Nonce Authority + let blockhash = Hash::default(); + let blockhash_string = format!("{}", blockhash); + let keypair = read_keypair_file(&keypair_file).unwrap(); + let test_pay = test_commands.clone().get_matches_from(vec![ + "test", + "pay", + &pubkey_string, + "50", + "lamports", + "--blockhash", + &blockhash_string, + "--nonce", + &pubkey_string, + "--nonce-authority", + &keypair_file, + ]); + assert_eq!( + parse_command(&test_pay).unwrap(), + CliCommandInfo { + command: CliCommand::Pay(PayCommand { + lamports: 50, + to: pubkey, + blockhash: Some(blockhash), + nonce_account: Some(pubkey), + nonce_authority: Some(keypair.into()), + ..PayCommand::default() + }), require_keypair: true } ); @@ -2360,17 +2457,14 @@ mod tests { assert_eq!( parse_command(&test_pay_multiple_witnesses).unwrap(), CliCommandInfo { - command: CliCommand::Pay { + command: CliCommand::Pay(PayCommand { lamports: 50, to: pubkey, timestamp: Some(dt), timestamp_pubkey: Some(witness0), witnesses: Some(vec![witness0, witness1]), - cancelable: false, - sign_only: false, - signers: None, - blockhash: None, - }, + ..PayCommand::default() + }), require_keypair: true } ); @@ -2494,6 +2588,8 @@ mod tests { sign_only: false, signers: None, blockhash: None, + nonce_account: None, + nonce_authority: None, }; let signature = process_command(&config); assert_eq!(signature.unwrap(), SIGNATURE.to_string()); @@ -2508,33 +2604,23 @@ mod tests { }; assert_eq!(process_command(&config).unwrap(), "1234"); - config.command = CliCommand::Pay { + config.command = CliCommand::Pay(PayCommand { lamports: 10, to: bob_pubkey, - timestamp: None, - timestamp_pubkey: None, - witnesses: None, - cancelable: false, - sign_only: false, - signers: None, - blockhash: None, - }; + ..PayCommand::default() + }); let signature = process_command(&config); assert_eq!(signature.unwrap(), SIGNATURE.to_string()); let date_string = "\"2018-09-19T17:30:59Z\""; let dt: DateTime = serde_json::from_str(&date_string).unwrap(); - config.command = CliCommand::Pay { + config.command = CliCommand::Pay(PayCommand { lamports: 10, to: bob_pubkey, timestamp: Some(dt), timestamp_pubkey: Some(config.keypair.pubkey()), - witnesses: None, - cancelable: false, - sign_only: false, - signers: None, - blockhash: None, - }; + ..PayCommand::default() + }); let result = process_command(&config); let json: Value = serde_json::from_str(&result.unwrap()).unwrap(); assert_eq!( @@ -2548,17 +2634,13 @@ mod tests { ); let witness = Pubkey::new_rand(); - config.command = CliCommand::Pay { + config.command = CliCommand::Pay(PayCommand { lamports: 10, to: bob_pubkey, - timestamp: None, - timestamp_pubkey: None, witnesses: Some(vec![witness]), cancelable: true, - sign_only: false, - signers: None, - blockhash: None, - }; + ..PayCommand::default() + }); let result = process_command(&config); let json: Value = serde_json::from_str(&result.unwrap()).unwrap(); assert_eq!( @@ -2571,6 +2653,57 @@ mod tests { SIGNATURE.to_string() ); + // Nonced pay + let blockhash = Hash::default(); + let nonce_response = json!(rpc_request::Response { + context: RpcResponseContext { slot: 1 }, + value: json!(Account::new_data( + 1, + &NonceState::Initialized(NonceMeta::new(&config.keypair.pubkey()), blockhash), + &nonce_program::ID, + ) + .unwrap()), + }); + let mut mocks = HashMap::new(); + mocks.insert(RpcRequest::GetAccountInfo, nonce_response); + config.rpc_client = Some(RpcClient::new_mock_with_mocks("".to_string(), mocks)); + config.command = CliCommand::Pay(PayCommand { + lamports: 10, + to: bob_pubkey, + nonce_account: Some(bob_pubkey), + blockhash: Some(blockhash), + ..PayCommand::default() + }); + let signature = process_command(&config); + assert_eq!(signature.unwrap(), SIGNATURE.to_string()); + + // Nonced pay w/ non-payer authority + let bob_keypair = Keypair::new(); + let bob_pubkey = bob_keypair.pubkey(); + let blockhash = Hash::default(); + let nonce_authority_response = json!(rpc_request::Response { + context: RpcResponseContext { slot: 1 }, + value: json!(Account::new_data( + 1, + &NonceState::Initialized(NonceMeta::new(&bob_pubkey), blockhash), + &nonce_program::ID, + ) + .unwrap()), + }); + let mut mocks = HashMap::new(); + mocks.insert(RpcRequest::GetAccountInfo, nonce_authority_response); + config.rpc_client = Some(RpcClient::new_mock_with_mocks("".to_string(), mocks)); + config.command = CliCommand::Pay(PayCommand { + lamports: 10, + to: bob_pubkey, + blockhash: Some(blockhash), + nonce_account: Some(bob_pubkey), + nonce_authority: Some(bob_keypair.into()), + ..PayCommand::default() + }); + let signature = process_command(&config); + assert_eq!(signature.unwrap(), SIGNATURE.to_string()); + let process_id = Pubkey::new_rand(); config.command = CliCommand::TimeElapsed(bob_pubkey, process_id, dt); let signature = process_command(&config); @@ -2669,43 +2802,29 @@ mod tests { }; assert!(process_command(&config).is_err()); - config.command = CliCommand::Pay { + config.command = CliCommand::Pay(PayCommand { lamports: 10, to: bob_pubkey, - timestamp: None, - timestamp_pubkey: None, - witnesses: None, - cancelable: false, - sign_only: false, - signers: None, - blockhash: None, - }; + ..PayCommand::default() + }); assert!(process_command(&config).is_err()); - config.command = CliCommand::Pay { + config.command = CliCommand::Pay(PayCommand { lamports: 10, to: bob_pubkey, timestamp: Some(dt), timestamp_pubkey: Some(config.keypair.pubkey()), - witnesses: None, - cancelable: false, - sign_only: false, - signers: None, - blockhash: None, - }; + ..PayCommand::default() + }); assert!(process_command(&config).is_err()); - config.command = CliCommand::Pay { + config.command = CliCommand::Pay(PayCommand { lamports: 10, to: bob_pubkey, - timestamp: None, - timestamp_pubkey: None, witnesses: Some(vec![witness]), cancelable: true, - sign_only: false, - signers: None, - blockhash: None, - }; + ..PayCommand::default() + }); assert!(process_command(&config).is_err()); config.command = CliCommand::TimeElapsed(bob_pubkey, process_id, dt); diff --git a/cli/src/nonce.rs b/cli/src/nonce.rs index f06f9ed9b..b1195fc4f 100644 --- a/cli/src/nonce.rs +++ b/cli/src/nonce.rs @@ -4,9 +4,10 @@ use crate::cli::{ CliError, ProcessResult, }; use clap::{App, Arg, ArgMatches, SubCommand}; -use solana_clap_utils::{input_parsers::*, input_validators::*}; +use solana_clap_utils::{input_parsers::*, input_validators::*, ArgConstant}; use solana_client::rpc_client::RpcClient; use solana_sdk::{ + account::Account, account_utils::State, hash::Hash, nonce_instruction::{authorize, create_nonce_account, nonce, withdraw, NonceError}, @@ -18,6 +19,30 @@ use solana_sdk::{ transaction::Transaction, }; +#[derive(Debug, Clone, PartialEq)] +pub enum CliNonceError { + InvalidAccountOwner, + InvalidAccountData, + InvalidHash, + InvalidAuthority, + InvalidState, +} + +pub const NONCE_ARG: ArgConstant<'static> = ArgConstant { + name: "nonce", + long: "nonce", + help: "Provide the nonce account to use when creating a nonced \n\ + transaction. Nonced transactions are useful when a transaction \n\ + requires a lengthy signing process. Learn more about nonced \n\ + transactions at https://docs.solana.com/offline-signing/durable-nonce", +}; + +pub const NONCE_AUTHORITY_ARG: ArgConstant<'static> = ArgConstant { + name: "nonce_authority", + long: "nonce-authority", + help: "Provide the nonce authority keypair to use when signing a nonced transaction", +}; + pub trait NonceSubCommands { fn nonce_subcommands(self) -> Self; } @@ -273,6 +298,34 @@ pub fn parse_withdraw_from_nonce_account( }) } +/// Check if a nonce account is initialized with the given authority and hash +pub fn check_nonce_account( + nonce_account: &Account, + nonce_authority: &Pubkey, + nonce_hash: &Hash, +) -> Result<(), Box> { + if nonce_account.owner != nonce_program::ID { + return Err(CliError::InvalidNonce(CliNonceError::InvalidAccountOwner).into()); + } + let nonce_state: NonceState = nonce_account + .state() + .map_err(|_| Box::new(CliError::InvalidNonce(CliNonceError::InvalidAccountData)))?; + match nonce_state { + NonceState::Initialized(meta, hash) => { + if &hash != nonce_hash { + Err(CliError::InvalidNonce(CliNonceError::InvalidHash).into()) + } else if nonce_authority != &meta.nonce_authority { + Err(CliError::InvalidNonce(CliNonceError::InvalidAuthority).into()) + } else { + Ok(()) + } + } + NonceState::Uninitialized => { + Err(CliError::InvalidNonce(CliNonceError::InvalidState).into()) + } + } +} + pub fn process_authorize_nonce_account( rpc_client: &RpcClient, config: &CliConfig, @@ -491,7 +544,13 @@ pub fn process_withdraw_from_nonce_account( mod tests { use super::*; use crate::cli::{app, parse_command}; - use solana_sdk::signature::{read_keypair_file, write_keypair}; + use solana_sdk::{ + account::Account, + hash::hash, + nonce_state::{Meta as NonceMeta, NonceState}, + signature::{read_keypair_file, write_keypair}, + system_program, + }; use tempfile::NamedTempFile; fn make_tmp_file() -> (String, NamedTempFile) { @@ -729,4 +788,66 @@ mod tests { } ); } + + #[test] + fn test_check_nonce_account() { + let blockhash = Hash::default(); + let nonce_pubkey = Pubkey::new_rand(); + let valid = Account::new_data( + 1, + &NonceState::Initialized(NonceMeta::new(&nonce_pubkey), blockhash), + &nonce_program::ID, + ); + assert!(check_nonce_account(&valid.unwrap(), &nonce_pubkey, &blockhash).is_ok()); + + let invalid_owner = Account::new_data( + 1, + &NonceState::Initialized(NonceMeta::new(&nonce_pubkey), blockhash), + &system_program::ID, + ); + assert_eq!( + check_nonce_account(&invalid_owner.unwrap(), &nonce_pubkey, &blockhash), + Err(Box::new(CliError::InvalidNonce( + CliNonceError::InvalidAccountOwner + ))), + ); + + let invalid_data = Account::new_data(1, &"invalid", &nonce_program::ID); + assert_eq!( + check_nonce_account(&invalid_data.unwrap(), &nonce_pubkey, &blockhash), + Err(Box::new(CliError::InvalidNonce( + CliNonceError::InvalidAccountData + ))), + ); + + let invalid_hash = Account::new_data( + 1, + &NonceState::Initialized(NonceMeta::new(&nonce_pubkey), hash(b"invalid")), + &nonce_program::ID, + ); + assert_eq!( + check_nonce_account(&invalid_hash.unwrap(), &nonce_pubkey, &blockhash), + Err(Box::new(CliError::InvalidNonce(CliNonceError::InvalidHash))), + ); + + let invalid_authority = Account::new_data( + 1, + &NonceState::Initialized(NonceMeta::new(&Pubkey::new_rand()), blockhash), + &nonce_program::ID, + ); + assert_eq!( + check_nonce_account(&invalid_authority.unwrap(), &nonce_pubkey, &blockhash), + Err(Box::new(CliError::InvalidNonce( + CliNonceError::InvalidAuthority + ))), + ); + + let invalid_state = Account::new_data(1, &NonceState::Uninitialized, &nonce_program::ID); + assert_eq!( + check_nonce_account(&invalid_state.unwrap(), &nonce_pubkey, &blockhash), + Err(Box::new(CliError::InvalidNonce( + CliNonceError::InvalidState + ))), + ); + } } diff --git a/cli/src/stake.rs b/cli/src/stake.rs index dffe927d3..a2d77b824 100644 --- a/cli/src/stake.rs +++ b/cli/src/stake.rs @@ -1,8 +1,11 @@ -use crate::cli::{ - build_balance_message, check_account_for_fee, check_unique_pubkeys, - get_blockhash_fee_calculator, log_instruction_custom_error, replace_signatures, - required_lamports_from, return_signers, CliCommand, CliCommandInfo, CliConfig, CliError, - ProcessResult, +use crate::{ + cli::{ + build_balance_message, check_account_for_fee, check_unique_pubkeys, + get_blockhash_fee_calculator, log_instruction_custom_error, replace_signatures, + required_lamports_from, return_signers, CliCommand, CliCommandInfo, CliConfig, CliError, + ProcessResult, + }, + nonce::{check_nonce_account, NONCE_ARG, NONCE_AUTHORITY_ARG}, }; use clap::{App, Arg, ArgMatches, SubCommand}; use console::style; @@ -153,6 +156,23 @@ impl StakeSubCommands for App<'_, '_> { .takes_value(true) .validator(is_hash) .help("Use the supplied blockhash"), + ) + .arg( + Arg::with_name(NONCE_ARG.name) + .long(NONCE_ARG.long) + .takes_value(true) + .value_name("PUBKEY") + .requires("blockhash") + .validator(is_pubkey) + .help(NONCE_ARG.help) + ) + .arg( + Arg::with_name(NONCE_AUTHORITY_ARG.name) + .long(NONCE_AUTHORITY_ARG.long) + .takes_value(true) + .requires(NONCE_ARG.name) + .validator(is_keypair_or_ask_keyword) + .help(NONCE_AUTHORITY_ARG.help) ), ) .subcommand( @@ -232,6 +252,23 @@ impl StakeSubCommands for App<'_, '_> { .takes_value(true) .validator(is_hash) .help("Use the supplied blockhash"), + ) + .arg( + Arg::with_name(NONCE_ARG.name) + .long(NONCE_ARG.long) + .takes_value(true) + .value_name("PUBKEY") + .requires("blockhash") + .validator(is_pubkey) + .help(NONCE_ARG.help) + ) + .arg( + Arg::with_name(NONCE_AUTHORITY_ARG.name) + .long(NONCE_AUTHORITY_ARG.long) + .takes_value(true) + .requires(NONCE_ARG.name) + .validator(is_keypair_or_ask_keyword) + .help(NONCE_AUTHORITY_ARG.help) ), ) .subcommand( @@ -360,6 +397,14 @@ pub fn parse_stake_delegate_stake(matches: &ArgMatches<'_>) -> Result) -> Result) -> Result) -> Result>, blockhash: Option, + nonce_account: Option, + nonce_authority: Option<&Keypair>, ) -> ProcessResult { let (recent_blockhash, fee_calculator) = get_blockhash_fee_calculator(rpc_client, sign_only, blockhash)?; @@ -568,18 +626,35 @@ pub fn process_deactivate_stake_account( stake_account_pubkey, &config.keypair.pubkey(), )]; - let mut tx = Transaction::new_signed_with_payer( - ixs, - Some(&config.keypair.pubkey()), - &[&config.keypair], - recent_blockhash, - ); + let mut tx = if let Some(nonce_account) = &nonce_account { + let nonce_authority: &Keypair = nonce_authority.unwrap_or(&config.keypair); + Transaction::new_signed_with_nonce( + ixs, + Some(&config.keypair.pubkey()), + &[&config.keypair, nonce_authority], + nonce_account, + &nonce_authority.pubkey(), + recent_blockhash, + ) + } else { + Transaction::new_signed_with_payer( + ixs, + Some(&config.keypair.pubkey()), + &[&config.keypair], + recent_blockhash, + ) + }; if let Some(signers) = signers { replace_signatures(&mut tx, &signers)?; } if sign_only { return_signers(&tx) } else { + if let Some(nonce_account) = &nonce_account { + let nonce_authority: &Keypair = nonce_authority.unwrap_or(&config.keypair); + let nonce_account = rpc_client.get_account(nonce_account)?; + check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?; + } check_account_for_fee( rpc_client, &tx.message.account_keys[0], @@ -760,6 +835,7 @@ pub fn process_show_stake_history( Ok("".to_string()) } +#[allow(clippy::too_many_arguments)] pub fn process_delegate_stake( rpc_client: &RpcClient, config: &CliConfig, @@ -769,6 +845,8 @@ pub fn process_delegate_stake( sign_only: bool, signers: &Option>, blockhash: Option, + nonce_account: Option, + nonce_authority: Option<&Keypair>, ) -> ProcessResult { check_unique_pubkeys( (&config.keypair.pubkey(), "cli keypair".to_string()), @@ -823,19 +901,35 @@ pub fn process_delegate_stake( &config.keypair.pubkey(), vote_account_pubkey, )]; - - let mut tx = Transaction::new_signed_with_payer( - ixs, - Some(&config.keypair.pubkey()), - &[&config.keypair], - recent_blockhash, - ); + let mut tx = if let Some(nonce_account) = &nonce_account { + let nonce_authority: &Keypair = nonce_authority.unwrap_or(&config.keypair); + Transaction::new_signed_with_nonce( + ixs, + Some(&config.keypair.pubkey()), + &[&config.keypair, nonce_authority], + nonce_account, + &nonce_authority.pubkey(), + recent_blockhash, + ) + } else { + Transaction::new_signed_with_payer( + ixs, + Some(&config.keypair.pubkey()), + &[&config.keypair], + recent_blockhash, + ) + }; if let Some(signers) = signers { replace_signatures(&mut tx, &signers)?; } if sign_only { return_signers(&tx) } else { + if let Some(nonce_account) = &nonce_account { + let nonce_authority: &Keypair = nonce_authority.unwrap_or(&config.keypair); + let nonce_account = rpc_client.get_account(nonce_account)?; + check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?; + } check_account_for_fee( rpc_client, &tx.message.account_keys[0], @@ -987,7 +1081,9 @@ mod tests { force: false, sign_only: false, signers: None, - blockhash: None + blockhash: None, + nonce_account: None, + nonce_authority: None, }, require_keypair: true } @@ -1009,7 +1105,9 @@ mod tests { force: true, sign_only: false, signers: None, - blockhash: None + blockhash: None, + nonce_account: None, + nonce_authority: None, }, require_keypair: true } @@ -1035,7 +1133,9 @@ mod tests { force: false, sign_only: false, signers: None, - blockhash: Some(blockhash) + blockhash: Some(blockhash), + nonce_account: None, + nonce_authority: None, }, require_keypair: true } @@ -1057,7 +1157,9 @@ mod tests { force: false, sign_only: true, signers: None, - blockhash: None + blockhash: None, + nonce_account: None, + nonce_authority: None, }, require_keypair: true } @@ -1084,7 +1186,9 @@ mod tests { force: false, sign_only: false, signers: Some(vec![(key1, sig1)]), - blockhash: None + blockhash: None, + nonce_account: None, + nonce_authority: None, }, require_keypair: false } @@ -1113,7 +1217,9 @@ mod tests { force: false, sign_only: false, signers: Some(vec![(key1, sig1), (key2, sig2)]), - blockhash: None + blockhash: None, + nonce_account: None, + nonce_authority: None, }, require_keypair: false } @@ -1150,7 +1256,9 @@ mod tests { stake_account_pubkey, sign_only: false, signers: None, - blockhash: None + blockhash: None, + nonce_account: None, + nonce_authority: None, }, require_keypair: true } @@ -1173,7 +1281,9 @@ mod tests { stake_account_pubkey, sign_only: false, signers: None, - blockhash: Some(blockhash) + blockhash: Some(blockhash), + nonce_account: None, + nonce_authority: None, }, require_keypair: true } @@ -1192,7 +1302,9 @@ mod tests { stake_account_pubkey, sign_only: true, signers: None, - blockhash: None + blockhash: None, + nonce_account: None, + nonce_authority: None, }, require_keypair: true } @@ -1216,7 +1328,9 @@ mod tests { stake_account_pubkey, sign_only: false, signers: Some(vec![(key1, sig1)]), - blockhash: None + blockhash: None, + nonce_account: None, + nonce_authority: None, }, require_keypair: false } @@ -1242,7 +1356,9 @@ mod tests { stake_account_pubkey, sign_only: false, signers: Some(vec![(key1, sig1), (key2, sig2)]), - blockhash: None + blockhash: None, + nonce_account: None, + nonce_authority: None, }, require_keypair: false } diff --git a/cli/tests/pay.rs b/cli/tests/pay.rs index 2179e8d14..4cfafd27a 100644 --- a/cli/tests/pay.rs +++ b/cli/tests/pay.rs @@ -1,9 +1,17 @@ use chrono::prelude::*; use serde_json::Value; -use solana_cli::cli::{process_command, request_and_confirm_airdrop, CliCommand, CliConfig}; +use solana_cli::cli::{ + process_command, request_and_confirm_airdrop, CliCommand, CliConfig, PayCommand, +}; use solana_client::rpc_client::RpcClient; use solana_faucet::faucet::run_local_faucet; -use solana_sdk::{hash::Hash, pubkey::Pubkey, signature::KeypairUtil, signature::Signature}; +use solana_sdk::{ + account_utils::State, + hash::Hash, + nonce_state::NonceState, + pubkey::Pubkey, + signature::{read_keypair_file, write_keypair, Keypair, KeypairUtil, Signature}, +}; use std::fs::remove_dir_all; use std::str::FromStr; use std::sync::mpsc::channel; @@ -12,6 +20,12 @@ use std::sync::mpsc::channel; use solana_core::validator::new_validator_for_tests; use std::thread::sleep; use std::time::Duration; +use tempfile::NamedTempFile; + +fn make_tmp_file() -> (String, NamedTempFile) { + let tmp_file = NamedTempFile::new().unwrap(); + (String::from(tmp_file.path().to_str().unwrap()), tmp_file) +} fn check_balance(expected_balance: u64, client: &RpcClient, pubkey: &Pubkey) { (0..5).for_each(|tries| { @@ -69,17 +83,13 @@ fn test_cli_timestamp_tx() { // Make transaction (from config_payer to bob_pubkey) requiring timestamp from config_witness let date_string = "\"2018-09-19T17:30:59Z\""; let dt: DateTime = serde_json::from_str(&date_string).unwrap(); - config_payer.command = CliCommand::Pay { + config_payer.command = CliCommand::Pay(PayCommand { lamports: 10, to: bob_pubkey, timestamp: Some(dt), timestamp_pubkey: Some(config_witness.keypair.pubkey()), - witnesses: None, - cancelable: false, - sign_only: false, - signers: None, - blockhash: None, - }; + ..PayCommand::default() + }); let sig_response = process_command(&config_payer); let object: Value = serde_json::from_str(&sig_response.unwrap()).unwrap(); @@ -144,17 +154,12 @@ fn test_cli_witness_tx() { .unwrap(); // Make transaction (from config_payer to bob_pubkey) requiring witness signature from config_witness - config_payer.command = CliCommand::Pay { + config_payer.command = CliCommand::Pay(PayCommand { lamports: 10, to: bob_pubkey, - timestamp: None, - timestamp_pubkey: None, witnesses: Some(vec![config_witness.keypair.pubkey()]), - cancelable: false, - sign_only: false, - signers: None, - blockhash: None, - }; + ..PayCommand::default() + }); let sig_response = process_command(&config_payer); let object: Value = serde_json::from_str(&sig_response.unwrap()).unwrap(); @@ -212,17 +217,13 @@ fn test_cli_cancel_tx() { .unwrap(); // Make transaction (from config_payer to bob_pubkey) requiring witness signature from config_witness - config_payer.command = CliCommand::Pay { + config_payer.command = CliCommand::Pay(PayCommand { lamports: 10, to: bob_pubkey, - timestamp: None, - timestamp_pubkey: None, witnesses: Some(vec![config_witness.keypair.pubkey()]), cancelable: true, - sign_only: false, - signers: None, - blockhash: None, - }; + ..PayCommand::default() + }); let sig_response = process_command(&config_payer).unwrap(); let object: Value = serde_json::from_str(&sig_response).unwrap(); @@ -288,17 +289,12 @@ fn test_offline_pay_tx() { check_balance(50, &rpc_client, &config_offline.keypair.pubkey()); check_balance(50, &rpc_client, &config_online.keypair.pubkey()); - config_offline.command = CliCommand::Pay { + config_offline.command = CliCommand::Pay(PayCommand { lamports: 10, to: bob_pubkey, - timestamp: None, - timestamp_pubkey: None, - witnesses: None, - cancelable: false, sign_only: true, - signers: None, - blockhash: None, - }; + ..PayCommand::default() + }); let sig_response = process_command(&config_offline).unwrap(); check_balance(50, &rpc_client, &config_offline.keypair.pubkey()); @@ -318,17 +314,13 @@ fn test_offline_pay_tx() { }) .collect(); - config_online.command = CliCommand::Pay { + config_online.command = CliCommand::Pay(PayCommand { 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::().unwrap()), - }; + ..PayCommand::default() + }); process_command(&config_online).unwrap(); check_balance(40, &rpc_client, &config_offline.keypair.pubkey()); @@ -338,3 +330,81 @@ fn test_offline_pay_tx() { server.close().unwrap(); remove_dir_all(ledger_path).unwrap(); } + +#[test] +fn test_nonced_pay_tx() { + solana_logger::setup(); + + let (server, leader_data, alice, ledger_path) = new_validator_for_tests(); + let (sender, receiver) = channel(); + run_local_faucet(alice, sender, None); + let faucet_addr = receiver.recv().unwrap(); + + let rpc_client = RpcClient::new_socket(leader_data.rpc); + + let mut config = CliConfig::default(); + config.json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); + + let minimum_nonce_balance = rpc_client + .get_minimum_balance_for_rent_exemption(NonceState::size()) + .unwrap(); + + request_and_confirm_airdrop( + &rpc_client, + &faucet_addr, + &config.keypair.pubkey(), + 50 + minimum_nonce_balance, + ) + .unwrap(); + check_balance( + 50 + minimum_nonce_balance, + &rpc_client, + &config.keypair.pubkey(), + ); + + // Create nonce account + let nonce_account = Keypair::new(); + let (nonce_keypair_file, mut tmp_file) = make_tmp_file(); + write_keypair(&nonce_account, tmp_file.as_file_mut()).unwrap(); + config.command = CliCommand::CreateNonceAccount { + nonce_account: read_keypair_file(&nonce_keypair_file).unwrap().into(), + nonce_authority: config.keypair.pubkey(), + lamports: minimum_nonce_balance, + }; + process_command(&config).unwrap(); + + check_balance(50, &rpc_client, &config.keypair.pubkey()); + check_balance(minimum_nonce_balance, &rpc_client, &nonce_account.pubkey()); + + // Fetch nonce hash + let account = rpc_client.get_account(&nonce_account.pubkey()).unwrap(); + let nonce_state: NonceState = account.state().unwrap(); + let nonce_hash = match nonce_state { + NonceState::Initialized(_meta, hash) => hash, + _ => panic!("Nonce is not initialized"), + }; + + let bob_pubkey = Pubkey::new_rand(); + config.command = CliCommand::Pay(PayCommand { + lamports: 10, + to: bob_pubkey, + blockhash: Some(nonce_hash), + nonce_account: Some(nonce_account.pubkey()), + ..PayCommand::default() + }); + process_command(&config).expect("failed to process pay command"); + + check_balance(40, &rpc_client, &config.keypair.pubkey()); + check_balance(10, &rpc_client, &bob_pubkey); + + // Verify that nonce has been used + let account = rpc_client.get_account(&nonce_account.pubkey()).unwrap(); + let nonce_state: NonceState = account.state().unwrap(); + match nonce_state { + NonceState::Initialized(_meta, hash) => assert_ne!(hash, nonce_hash), + _ => assert!(false, "Nonce is not initialized"), + } + + server.close().unwrap(); + remove_dir_all(ledger_path).unwrap(); +} diff --git a/cli/tests/stake.rs b/cli/tests/stake.rs index 80774e7d8..cf6a9dd8f 100644 --- a/cli/tests/stake.rs +++ b/cli/tests/stake.rs @@ -3,9 +3,11 @@ use solana_cli::cli::{process_command, request_and_confirm_airdrop, CliCommand, use solana_client::rpc_client::RpcClient; use solana_faucet::faucet::run_local_faucet; use solana_sdk::{ + account_utils::State, hash::Hash, + nonce_state::NonceState, pubkey::Pubkey, - signature::{read_keypair_file, write_keypair, KeypairUtil, Signature}, + signature::{read_keypair_file, write_keypair, Keypair, KeypairUtil, Signature}, }; use solana_stake_program::stake_state::Lockup; use std::fs::remove_dir_all; @@ -101,6 +103,8 @@ fn test_stake_delegation_and_deactivation() { sign_only: false, signers: None, blockhash: None, + nonce_account: None, + nonce_authority: None, }; process_command(&config_validator).unwrap(); @@ -110,6 +114,8 @@ fn test_stake_delegation_and_deactivation() { sign_only: false, signers: None, blockhash: None, + nonce_account: None, + nonce_authority: None, }; process_command(&config_validator).unwrap(); @@ -185,6 +191,8 @@ fn test_stake_delegation_and_deactivation_offline() { sign_only: true, signers: None, blockhash: None, + nonce_account: None, + nonce_authority: None, }; let sig_response = process_command(&config_validator).unwrap(); let object: Value = serde_json::from_str(&sig_response).unwrap(); @@ -208,6 +216,8 @@ fn test_stake_delegation_and_deactivation_offline() { sign_only: false, signers: Some(signers), blockhash: Some(blockhash_str.parse::().unwrap()), + nonce_account: None, + nonce_authority: None, }; process_command(&config_payer).unwrap(); @@ -217,6 +227,8 @@ fn test_stake_delegation_and_deactivation_offline() { sign_only: true, signers: None, blockhash: None, + nonce_account: None, + nonce_authority: None, }; let sig_response = process_command(&config_validator).unwrap(); let object: Value = serde_json::from_str(&sig_response).unwrap(); @@ -238,9 +250,114 @@ fn test_stake_delegation_and_deactivation_offline() { sign_only: false, signers: Some(signers), blockhash: Some(blockhash_str.parse::().unwrap()), + nonce_account: None, + nonce_authority: None, }; process_command(&config_payer).unwrap(); server.close().unwrap(); remove_dir_all(ledger_path).unwrap(); } + +#[test] +fn test_nonced_stake_delegation_and_deactivation() { + solana_logger::setup(); + + let (server, leader_data, alice, ledger_path) = new_validator_for_tests(); + let (sender, receiver) = channel(); + run_local_faucet(alice, sender, None); + let faucet_addr = receiver.recv().unwrap(); + + let rpc_client = RpcClient::new_socket(leader_data.rpc); + + let mut config = CliConfig::default(); + config.json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); + + let minimum_nonce_balance = rpc_client + .get_minimum_balance_for_rent_exemption(NonceState::size()) + .unwrap(); + + request_and_confirm_airdrop(&rpc_client, &faucet_addr, &config.keypair.pubkey(), 100_000) + .unwrap(); + + // Create vote account + let vote_keypair = Keypair::new(); + let (vote_keypair_file, mut tmp_file) = make_tmp_file(); + write_keypair(&vote_keypair, tmp_file.as_file_mut()).unwrap(); + config.command = CliCommand::CreateVoteAccount { + vote_account: read_keypair_file(&vote_keypair_file).unwrap().into(), + node_pubkey: config.keypair.pubkey(), + authorized_voter: None, + authorized_withdrawer: None, + commission: 0, + }; + process_command(&config).unwrap(); + + // Create stake account + let stake_keypair = Keypair::new(); + let (stake_keypair_file, mut tmp_file) = make_tmp_file(); + write_keypair(&stake_keypair, tmp_file.as_file_mut()).unwrap(); + config.command = CliCommand::CreateStakeAccount { + stake_account: read_keypair_file(&stake_keypair_file).unwrap().into(), + staker: None, + withdrawer: None, + lockup: Lockup::default(), + lamports: 50_000, + }; + process_command(&config).unwrap(); + + // Create nonce account + let nonce_account = Keypair::new(); + let (nonce_keypair_file, mut tmp_file) = make_tmp_file(); + write_keypair(&nonce_account, tmp_file.as_file_mut()).unwrap(); + config.command = CliCommand::CreateNonceAccount { + nonce_account: read_keypair_file(&nonce_keypair_file).unwrap().into(), + nonce_authority: config.keypair.pubkey(), + lamports: minimum_nonce_balance, + }; + process_command(&config).unwrap(); + + // Fetch nonce hash + let account = rpc_client.get_account(&nonce_account.pubkey()).unwrap(); + let nonce_state: NonceState = account.state().unwrap(); + let nonce_hash = match nonce_state { + NonceState::Initialized(_meta, hash) => hash, + _ => panic!("Nonce is not initialized"), + }; + + // Delegate stake + config.command = CliCommand::DelegateStake { + stake_account_pubkey: stake_keypair.pubkey(), + vote_account_pubkey: vote_keypair.pubkey(), + force: true, + sign_only: false, + signers: None, + blockhash: Some(nonce_hash), + nonce_account: Some(nonce_account.pubkey()), + nonce_authority: None, + }; + process_command(&config).unwrap(); + + // Fetch nonce hash + let account = rpc_client.get_account(&nonce_account.pubkey()).unwrap(); + let nonce_state: NonceState = account.state().unwrap(); + let nonce_hash = match nonce_state { + NonceState::Initialized(_meta, hash) => hash, + _ => panic!("Nonce is not initialized"), + }; + + // Deactivate stake + let config_keypair = Keypair::from_bytes(&config.keypair.to_bytes()).unwrap(); + config.command = CliCommand::DeactivateStake { + stake_account_pubkey: stake_keypair.pubkey(), + sign_only: false, + signers: None, + blockhash: Some(nonce_hash), + nonce_account: Some(nonce_account.pubkey()), + nonce_authority: Some(config_keypair.into()), + }; + process_command(&config).unwrap(); + + 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 72dacddba..0cda46712 100644 --- a/client/src/mock_rpc_client_request.rs +++ b/client/src/mock_rpc_client_request.rs @@ -9,18 +9,28 @@ use solana_sdk::{ instruction::InstructionError, transaction::{self, TransactionError}, }; +use std::{collections::HashMap, sync::RwLock}; pub const PUBKEY: &str = "7RoSF9fUmdphVCpabEoefH81WwrW7orsWonXWqTXkKV8"; pub const SIGNATURE: &str = "43yNSFC6fYTuPgTNFFhF4axw7AfWxB2BPdurme8yrsWEYwm8299xh8n6TAHjGymiSub1XtyxTNyd9GBfY2hxoBw8"; +pub type Mocks = HashMap; pub struct MockRpcClientRequest { + mocks: RwLock, url: String, } impl MockRpcClientRequest { pub fn new(url: String) -> Self { - Self { url } + Self::new_with_mocks(url, Mocks::default()) + } + + pub fn new_with_mocks(url: String, mocks: Mocks) -> Self { + Self { + url, + mocks: RwLock::new(mocks), + } } } @@ -31,6 +41,9 @@ impl GenericRpcClientRequest for MockRpcClientRequest { params: serde_json::Value, _retries: usize, ) -> Result { + if let Some(value) = self.mocks.write().unwrap().remove(request) { + return Ok(value); + } if self.url == "fails" { return Ok(Value::Null); } diff --git a/client/src/rpc_client.rs b/client/src/rpc_client.rs index 56ac69b26..f2504c2f8 100644 --- a/client/src/rpc_client.rs +++ b/client/src/rpc_client.rs @@ -2,7 +2,7 @@ use crate::rpc_request::{Response, RpcResponse}; use crate::{ client_error::ClientError, generic_rpc_client_request::GenericRpcClientRequest, - mock_rpc_client_request::MockRpcClientRequest, + mock_rpc_client_request::{MockRpcClientRequest, Mocks}, rpc_client_request::RpcClientRequest, rpc_request::{ RpcConfirmedBlock, RpcContactInfo, RpcEpochInfo, RpcLeaderSchedule, RpcRequest, @@ -48,6 +48,12 @@ impl RpcClient { } } + pub fn new_mock_with_mocks(url: String, mocks: Mocks) -> Self { + Self { + client: Box::new(MockRpcClientRequest::new_with_mocks(url, mocks)), + } + } + pub fn new_socket(addr: SocketAddr) -> Self { Self::new(get_rpc_request_str(addr, false)) } diff --git a/client/src/rpc_request.rs b/client/src/rpc_request.rs index da60f4942..4bd370132 100644 --- a/client/src/rpc_request.rs +++ b/client/src/rpc_request.rs @@ -113,7 +113,7 @@ pub struct RpcVoteAccountInfo { pub root_slot: Slot, } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq, Hash)] pub enum RpcRequest { ConfirmTransaction, DeregisterNode, diff --git a/sdk/src/system_transaction.rs b/sdk/src/system_transaction.rs index aab1bd2b8..e5b112d62 100644 --- a/sdk/src/system_transaction.rs +++ b/sdk/src/system_transaction.rs @@ -49,3 +49,25 @@ pub fn transfer( let instructions = vec![transfer_instruction]; Transaction::new_signed_instructions(&[from_keypair], instructions, recent_blockhash) } + +/// Create and sign new nonced system_instruction::Transfer transaction +pub fn nonced_transfer( + from_keypair: &Keypair, + to: &Pubkey, + lamports: u64, + nonce_account: &Pubkey, + nonce_authority: &Keypair, + nonce_hash: Hash, +) -> Transaction { + let from_pubkey = from_keypair.pubkey(); + let transfer_instruction = system_instruction::transfer(&from_pubkey, to, lamports); + let instructions = vec![transfer_instruction]; + Transaction::new_signed_with_nonce( + instructions, + Some(&from_pubkey), + &[from_keypair, nonce_authority], + nonce_account, + &nonce_authority.pubkey(), + nonce_hash, + ) +} diff --git a/sdk/src/transaction.rs b/sdk/src/transaction.rs index 631687247..15dfa0be6 100644 --- a/sdk/src/transaction.rs +++ b/sdk/src/transaction.rs @@ -3,6 +3,7 @@ use crate::hash::Hash; use crate::instruction::{CompiledInstruction, Instruction, InstructionError}; use crate::message::Message; +use crate::nonce_instruction; use crate::pubkey::Pubkey; use crate::short_vec; use crate::signature::{KeypairUtil, Signature}; @@ -94,6 +95,19 @@ impl Transaction { Self::new(signing_keypairs, message, recent_blockhash) } + pub fn new_signed_with_nonce( + mut instructions: Vec, + payer: Option<&Pubkey>, + signing_keypairs: &[&T], + nonce_account_pubkey: &Pubkey, + nonce_authority_pubkey: &Pubkey, + nonce_hash: Hash, + ) -> Self { + let nonce_ix = nonce_instruction::nonce(&nonce_account_pubkey, &nonce_authority_pubkey); + instructions.insert(0, nonce_ix); + Self::new_signed_with_payer(instructions, payer, signing_keypairs, nonce_hash) + } + pub fn new_unsigned_instructions(instructions: Vec) -> Self { let message = Message::new(instructions); Self::new_unsigned(message)