diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 4968fb1a8a..090a5534ca 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -273,12 +273,19 @@ pub enum CliCommand { Deploy(String), // Stake Commands CreateStakeAccount { - stake_account: KeypairEq, + stake_account: SigningAuthority, seed: Option, staker: Option, withdrawer: Option, lockup: Lockup, lamports: u64, + sign_only: bool, + signers: Option>, + blockhash_query: BlockhashQuery, + nonce_account: Option, + nonce_authority: Option, + fee_payer: Option, + from: Option, }, DeactivateStake { stake_account_pubkey: Pubkey, @@ -350,6 +357,12 @@ pub enum CliCommand { destination_account_pubkey: Pubkey, lamports: u64, withdraw_authority: Option, + sign_only: bool, + signers: Option>, + blockhash_query: BlockhashQuery, + nonce_account: Option, + nonce_authority: Option, + fee_payer: Option, }, // Storage Commands CreateStorageAccount { @@ -1551,12 +1564,19 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { // Create stake account CliCommand::CreateStakeAccount { - stake_account, + ref stake_account, seed, staker, withdrawer, lockup, lamports, + sign_only, + ref signers, + blockhash_query, + ref nonce_account, + ref nonce_authority, + ref fee_payer, + ref from, } => process_create_stake_account( &rpc_client, config, @@ -1566,6 +1586,13 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { withdrawer, lockup, *lamports, + *sign_only, + signers.as_ref(), + blockhash_query, + nonce_account.as_ref(), + nonce_authority.as_ref(), + fee_payer.as_ref(), + from.as_ref(), ), CliCommand::DeactivateStake { stake_account_pubkey, @@ -1705,6 +1732,12 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { destination_account_pubkey, lamports, ref withdraw_authority, + sign_only, + ref signers, + blockhash_query, + ref nonce_account, + ref nonce_authority, + ref fee_payer, } => process_withdraw_stake( &rpc_client, config, @@ -1712,6 +1745,12 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { &destination_account_pubkey, *lamports, withdraw_authority.as_ref(), + *sign_only, + signers.as_ref(), + blockhash_query, + nonce_account.as_ref(), + nonce_authority.as_ref(), + fee_payer.as_ref(), ), // Storage Commands @@ -3048,6 +3087,13 @@ mod tests { custodian, }, lamports: 1234, + sign_only: false, + signers: None, + blockhash_query: BlockhashQuery::All, + nonce_account: None, + nonce_authority: None, + fee_payer: None, + from: None, }; let signature = process_command(&config); assert_eq!(signature.unwrap(), SIGNATURE.to_string()); @@ -3059,6 +3105,12 @@ mod tests { destination_account_pubkey: to_pubkey, lamports: 100, withdraw_authority: None, + sign_only: false, + signers: None, + blockhash_query: BlockhashQuery::All, + nonce_account: None, + nonce_authority: None, + fee_payer: None, }; let signature = process_command(&config); assert_eq!(signature.unwrap(), SIGNATURE.to_string()); diff --git a/cli/src/stake.rs b/cli/src/stake.rs index e9aab398b7..b9e22e120c 100644 --- a/cli/src/stake.rs +++ b/cli/src/stake.rs @@ -76,8 +76,8 @@ impl StakeSubCommands for App<'_, '_> { .value_name("STAKE ACCOUNT") .takes_value(true) .required(true) - .validator(is_keypair_or_ask_keyword) - .help("Keypair of the stake account to fund") + .validator(is_pubkey_or_keypair_or_ask_keyword) + .help("Signing authority of the stake address to fund") ) .arg( Arg::with_name("amount") @@ -142,6 +142,18 @@ impl StakeSubCommands for App<'_, '_> { .validator(is_pubkey_or_keypair) .help(WITHDRAW_AUTHORITY_ARG.help) ) + .arg( + Arg::with_name("from") + .long("from") + .takes_value(true) + .value_name("KEYPAIR or PUBKEY") + .validator(is_pubkey_or_keypair_or_ask_keyword) + .help("Source account of funds (if different from client local account)"), + ) + .offline_args() + .arg(nonce_arg()) + .arg(nonce_authority_arg()) + .arg(fee_payer_arg()) ) .subcommand( SubCommand::with_name("delegate-stake") @@ -337,6 +349,10 @@ impl StakeSubCommands for App<'_, '_> { .help("Specify unit to use for request") ) .arg(withdraw_authority_arg()) + .offline_args() + .arg(nonce_arg()) + .arg(nonce_authority_arg()) + .arg(fee_payer_arg()) ) .subcommand( SubCommand::with_name("stake-set-lockup") @@ -421,7 +437,6 @@ impl StakeSubCommands for App<'_, '_> { } pub fn parse_stake_create_account(matches: &ArgMatches<'_>) -> Result { - let stake_account = keypair_of(matches, "stake_account").unwrap(); let seed = matches.value_of("seed").map(|s| s.to_string()); let epoch = value_of(&matches, "lockup_epoch").unwrap_or(0); let unix_timestamp = unix_timestamp_from_rfc3339_datetime(&matches, "lockup_date").unwrap_or(0); @@ -429,10 +444,24 @@ pub fn parse_stake_create_account(matches: &ArgMatches<'_>) -> Result) -> Result) -> Result) -> Result) -> Result, staker: &Option, withdrawer: &Option, lockup: &Lockup, lamports: u64, + sign_only: bool, + signers: Option<&Vec<(Pubkey, Signature)>>, + blockhash_query: &BlockhashQuery, + nonce_account: Option<&Pubkey>, + nonce_authority: Option<&SigningAuthority>, + fee_payer: Option<&SigningAuthority>, + from: Option<&SigningAuthority>, ) -> ProcessResult { - let stake_account_pubkey = stake_account.pubkey(); + // Offline derived address creation currently is not possible + // https://github.com/solana-labs/solana/pull/8252 + if seed.is_some() && (sign_only || signers.is_some()) { + return Err(CliError::BadParameter( + "Offline stake account creation with derived addresses are not yet supported" + .to_string(), + ) + .into()); + } + + let (stake_account_pubkey, stake_account) = (stake_account.pubkey(), stake_account.keypair()); let stake_account_address = if let Some(seed) = seed { create_address_with_seed(&stake_account_pubkey, &seed, &solana_stake_program::id())? } else { stake_account_pubkey }; + let (from_pubkey, from) = from + .map(|f| (f.pubkey(), f.keypair())) + .unwrap_or((config.keypair.pubkey(), &config.keypair)); check_unique_pubkeys( - (&config.keypair.pubkey(), "cli keypair".to_string()), + (&from_pubkey, "from keypair".to_string()), (&stake_account_address, "stake_account".to_string()), )?; - if let Ok(stake_account) = rpc_client.get_account(&stake_account_address) { - let err_msg = if stake_account.owner == solana_stake_program::id() { - format!("Stake account {} already exists", stake_account_address) - } else { - format!( - "Account {} already exists and is not a stake account", - stake_account_address - ) - }; - return Err(CliError::BadParameter(err_msg).into()); - } + if !sign_only { + if let Ok(stake_account) = rpc_client.get_account(&stake_account_address) { + let err_msg = if stake_account.owner == solana_stake_program::id() { + format!("Stake account {} already exists", stake_account_address) + } else { + format!( + "Account {} already exists and is not a stake account", + stake_account_address + ) + }; + return Err(CliError::BadParameter(err_msg).into()); + } - let minimum_balance = - rpc_client.get_minimum_balance_for_rent_exemption(std::mem::size_of::())?; + let minimum_balance = + rpc_client.get_minimum_balance_for_rent_exemption(std::mem::size_of::())?; - if lamports < minimum_balance { - return Err(CliError::BadParameter(format!( - "need at least {} lamports for stake account to be rent exempt, provided lamports: {}", - minimum_balance, lamports - )) - .into()); + if lamports < minimum_balance { + return Err(CliError::BadParameter(format!( + "need at least {} lamports for stake account to be rent exempt, provided lamports: {}", + minimum_balance, lamports + )) + .into()); + } } let authorized = Authorized { - staker: staker.unwrap_or(config.keypair.pubkey()), - withdrawer: withdrawer.unwrap_or(config.keypair.pubkey()), + staker: staker.unwrap_or(from_pubkey), + withdrawer: withdrawer.unwrap_or(from_pubkey), }; let ixs = if let Some(seed) = seed { stake_instruction::create_account_with_seed( - &config.keypair.pubkey(), // from - &stake_account_address, // to - &stake_account_pubkey, // base - seed, // seed + &from.pubkey(), // from + &stake_account_address, // to + &stake_account.pubkey(), // base + seed, // seed &authorized, lockup, lamports, ) } else { stake_instruction::create_account( - &config.keypair.pubkey(), - &stake_account_pubkey, + &from.pubkey(), + &stake_account.pubkey(), &authorized, lockup, lamports, ) }; - let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; + let (recent_blockhash, fee_calculator) = + blockhash_query.get_blockhash_fee_calculator(rpc_client)?; - let signers = if stake_account_pubkey != config.keypair.pubkey() { - vec![&config.keypair, stake_account] // both must sign if `from` and `to` differ + let fee_payer = fee_payer.map(|fp| fp.keypair()).unwrap_or(&config.keypair); + let mut tx_signers = if stake_account_pubkey != from_pubkey { + vec![fee_payer, from, stake_account] // both must sign if `from` and `to` differ } else { - vec![&config.keypair] // when stake_account == config.keypair and there's a seed, we only need one signature + vec![fee_payer, from] // when stake_account == config.keypair and there's a seed, we only need one signature }; + let (nonce_authority_pubkey, nonce_authority) = nonce_authority + .map(|na| (na.pubkey(), na.keypair())) + .unwrap_or((config.keypair.pubkey(), &config.keypair)); - let mut tx = Transaction::new_signed_with_payer( - ixs, - Some(&config.keypair.pubkey()), - &signers, - recent_blockhash, - ); - check_account_for_fee( - rpc_client, - &config.keypair.pubkey(), - &fee_calculator, - &tx.message, - )?; - let result = rpc_client.send_and_confirm_transaction(&mut tx, &signers); - log_instruction_custom_error::(result) + let mut tx = if let Some(nonce_account) = &nonce_account { + tx_signers.push(nonce_authority); + Transaction::new_signed_with_nonce( + ixs, + Some(&fee_payer.pubkey()), + &tx_signers, + nonce_account, + &nonce_authority.pubkey(), + recent_blockhash, + ) + } else { + Transaction::new_signed_with_payer( + ixs, + Some(&fee_payer.pubkey()), + &tx_signers, + 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_account = rpc_client.get_account(nonce_account)?; + check_nonce_account(&nonce_account, &nonce_authority_pubkey, &recent_blockhash)?; + } + check_account_for_fee( + rpc_client, + &tx.message.account_keys[0], + &fee_calculator, + &tx.message, + )?; + let result = rpc_client.send_and_confirm_transaction(&mut tx, &tx_signers); + log_instruction_custom_error::(result) + } } #[allow(clippy::too_many_arguments)] @@ -886,6 +991,7 @@ pub fn process_deactivate_stake_account( } } +#[allow(clippy::too_many_arguments)] pub fn process_withdraw_stake( rpc_client: &RpcClient, config: &CliConfig, @@ -893,8 +999,15 @@ pub fn process_withdraw_stake( destination_account_pubkey: &Pubkey, lamports: u64, withdraw_authority: Option<&SigningAuthority>, + sign_only: bool, + signers: Option<&Vec<(Pubkey, Signature)>>, + blockhash_query: &BlockhashQuery, + nonce_account: Option<&Pubkey>, + nonce_authority: Option<&SigningAuthority>, + fee_payer: Option<&SigningAuthority>, ) -> ProcessResult { - let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; + let (recent_blockhash, fee_calculator) = + blockhash_query.get_blockhash_fee_calculator(rpc_client)?; let withdraw_authority = withdraw_authority .map(|a| a.keypair()) .unwrap_or(&config.keypair); @@ -906,20 +1019,46 @@ pub fn process_withdraw_stake( lamports, )]; - let mut tx = Transaction::new_signed_with_payer( - ixs, - Some(&config.keypair.pubkey()), - &[&config.keypair, withdraw_authority], - recent_blockhash, - ); - check_account_for_fee( - rpc_client, - &config.keypair.pubkey(), - &fee_calculator, - &tx.message, - )?; - let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair]); - log_instruction_custom_error::(result) + let fee_payer = fee_payer.map(|fp| fp.keypair()).unwrap_or(&config.keypair); + let (nonce_authority_pubkey, nonce_authority) = nonce_authority + .map(|na| (na.pubkey(), na.keypair())) + .unwrap_or((config.keypair.pubkey(), &config.keypair)); + let mut tx = if let Some(nonce_account) = &nonce_account { + Transaction::new_signed_with_nonce( + ixs, + Some(&fee_payer.pubkey()), + &[fee_payer, withdraw_authority, nonce_authority], + nonce_account, + &nonce_authority.pubkey(), + recent_blockhash, + ) + } else { + Transaction::new_signed_with_payer( + ixs, + Some(&fee_payer.pubkey()), + &[fee_payer, withdraw_authority], + 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_account = rpc_client.get_account(nonce_account)?; + check_nonce_account(&nonce_account, &nonce_authority_pubkey, &recent_blockhash)?; + } + check_account_for_fee( + rpc_client, + &tx.message.account_keys[0], + &fee_calculator, + &tx.message, + )?; + let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair]); + log_instruction_custom_error::(result) + } } #[allow(clippy::too_many_arguments)] @@ -1741,7 +1880,14 @@ mod tests { unix_timestamp: 0, custodian, }, - lamports: 50 + lamports: 50, + sign_only: false, + signers: None, + blockhash_query: BlockhashQuery::All, + nonce_account: None, + nonce_authority: None, + fee_payer: None, + from: None, }, require_keypair: true } @@ -1765,17 +1911,76 @@ mod tests { parse_command(&test_create_stake_account2).unwrap(), CliCommandInfo { command: CliCommand::CreateStakeAccount { - stake_account: stake_account_keypair.into(), + stake_account: read_keypair_file(&keypair_file).unwrap().into(), seed: None, staker: None, withdrawer: None, lockup: Lockup::default(), - lamports: 50 + lamports: 50, + sign_only: false, + signers: None, + blockhash_query: BlockhashQuery::All, + nonce_account: None, + nonce_authority: None, + fee_payer: None, + from: None, }, require_keypair: true } ); + // CreateStakeAccount offline and nonce + let nonce_account = Pubkey::new(&[1u8; 32]); + let nonce_account_string = nonce_account.to_string(); + let offline = keypair_from_seed(&[2u8; 32]).unwrap(); + let offline_pubkey = offline.pubkey(); + let offline_string = offline_pubkey.to_string(); + let offline_sig = offline.sign_message(&[3u8]); + let offline_signer = format!("{}={}", offline_pubkey, offline_sig); + let nonce_hash = Hash::new(&[4u8; 32]); + let nonce_hash_string = nonce_hash.to_string(); + let test_create_stake_account2 = test_commands.clone().get_matches_from(vec![ + "test", + "create-stake-account", + &keypair_file, + "50", + "lamports", + "--blockhash", + &nonce_hash_string, + "--nonce", + &nonce_account_string, + "--nonce-authority", + &offline_string, + "--fee-payer", + &offline_string, + "--from", + &offline_string, + "--signer", + &offline_signer, + ]); + + assert_eq!( + parse_command(&test_create_stake_account2).unwrap(), + CliCommandInfo { + command: CliCommand::CreateStakeAccount { + stake_account: read_keypair_file(&keypair_file).unwrap().into(), + seed: None, + staker: None, + withdrawer: None, + lockup: Lockup::default(), + lamports: 50, + sign_only: false, + signers: Some(vec![(offline_pubkey, offline_sig)]), + blockhash_query: BlockhashQuery::FeeCalculator(nonce_hash), + nonce_account: Some(nonce_account), + nonce_authority: Some(offline_pubkey.into()), + fee_payer: Some(offline_pubkey.into()), + from: Some(offline_pubkey.into()), + }, + require_keypair: false, + } + ); + // Test DelegateStake Subcommand let vote_account_pubkey = Pubkey::new_rand(); let vote_account_string = vote_account_pubkey.to_string(); @@ -2076,6 +2281,12 @@ mod tests { destination_account_pubkey: stake_account_pubkey, lamports: 42, withdraw_authority: None, + sign_only: false, + signers: None, + blockhash_query: BlockhashQuery::All, + nonce_account: None, + nonce_authority: None, + fee_payer: None, }, require_keypair: true } @@ -2105,11 +2316,62 @@ mod tests { .unwrap() .into() ), + sign_only: false, + signers: None, + blockhash_query: BlockhashQuery::All, + nonce_account: None, + nonce_authority: None, + fee_payer: None, }, require_keypair: true } ); + // Test WithdrawStake Subcommand w/ authority and offline nonce + let test_withdraw_stake = test_commands.clone().get_matches_from(vec![ + "test", + "withdraw-stake", + &stake_account_string, + &stake_account_string, + "42", + "lamports", + "--withdraw-authority", + &stake_authority_keypair_file, + "--blockhash", + &nonce_hash_string, + "--nonce", + &nonce_account_string, + "--nonce-authority", + &offline_string, + "--fee-payer", + &offline_string, + "--signer", + &offline_signer, + ]); + + assert_eq!( + parse_command(&test_withdraw_stake).unwrap(), + CliCommandInfo { + command: CliCommand::WithdrawStake { + stake_account_pubkey, + destination_account_pubkey: stake_account_pubkey, + lamports: 42, + withdraw_authority: Some( + read_keypair_file(&stake_authority_keypair_file) + .unwrap() + .into() + ), + sign_only: false, + signers: Some(vec![(offline_pubkey, offline_sig)]), + blockhash_query: BlockhashQuery::FeeCalculator(nonce_hash), + nonce_account: Some(nonce_account), + nonce_authority: Some(offline_pubkey.into()), + fee_payer: Some(offline_pubkey.into()), + }, + require_keypair: false, + } + ); + // Test DeactivateStake Subcommand let test_deactivate_stake = test_commands.clone().get_matches_from(vec![ "test", diff --git a/cli/tests/stake.rs b/cli/tests/stake.rs index d9dc880031..890a97a01a 100644 --- a/cli/tests/stake.rs +++ b/cli/tests/stake.rs @@ -83,6 +83,13 @@ fn test_stake_delegation_force() { withdrawer: None, lockup: Lockup::default(), lamports: 50_000, + sign_only: false, + signers: None, + blockhash_query: BlockhashQuery::All, + nonce_account: None, + nonce_authority: None, + fee_payer: None, + from: None, }; process_command(&config).unwrap(); @@ -168,6 +175,13 @@ fn test_seed_stake_delegation_and_deactivation() { withdrawer: None, lockup: Lockup::default(), lamports: 50_000, + sign_only: false, + signers: None, + blockhash_query: BlockhashQuery::All, + nonce_account: None, + nonce_authority: None, + fee_payer: None, + from: None, }; process_command(&config_validator).unwrap(); @@ -242,6 +256,13 @@ fn test_stake_delegation_and_deactivation() { withdrawer: None, lockup: Lockup::default(), lamports: 50_000, + sign_only: false, + signers: None, + blockhash_query: BlockhashQuery::All, + nonce_account: None, + nonce_authority: None, + fee_payer: None, + from: None, }; process_command(&config_validator).unwrap(); @@ -335,6 +356,13 @@ fn test_offline_stake_delegation_and_deactivation() { withdrawer: None, lockup: Lockup::default(), lamports: 50_000, + sign_only: false, + signers: None, + blockhash_query: BlockhashQuery::All, + nonce_account: None, + nonce_authority: None, + fee_payer: None, + from: None, }; process_command(&config_validator).unwrap(); @@ -431,6 +459,13 @@ fn test_nonced_stake_delegation_and_deactivation() { withdrawer: None, lockup: Lockup::default(), lamports: 50_000, + sign_only: false, + signers: None, + blockhash_query: BlockhashQuery::All, + nonce_account: None, + nonce_authority: None, + fee_payer: None, + from: None, }; process_command(&config).unwrap(); @@ -538,6 +573,13 @@ fn test_stake_authorize() { withdrawer: None, lockup: Lockup::default(), lamports: 50_000, + sign_only: false, + signers: None, + blockhash_query: BlockhashQuery::All, + nonce_account: None, + nonce_authority: None, + fee_payer: None, + from: None, }; process_command(&config).unwrap(); @@ -761,6 +803,13 @@ fn test_stake_authorize_with_fee_payer() { withdrawer: None, lockup: Lockup::default(), lamports: 50_000, + sign_only: false, + signers: None, + blockhash_query: BlockhashQuery::All, + nonce_account: None, + nonce_authority: None, + fee_payer: None, + from: None, }; process_command(&config).unwrap(); // `config` balance should be 50,000 - 1 stake account sig - 1 fee sig @@ -890,6 +939,13 @@ fn test_stake_split() { withdrawer: Some(offline_pubkey), lockup: Lockup::default(), lamports: 10 * minimum_stake_balance, + sign_only: false, + signers: None, + blockhash_query: BlockhashQuery::All, + nonce_account: None, + nonce_authority: None, + fee_payer: None, + from: None, }; process_command(&config).unwrap(); check_balance( @@ -1022,6 +1078,13 @@ fn test_stake_set_lockup() { withdrawer: Some(offline_pubkey), lockup, lamports: 10 * minimum_stake_balance, + sign_only: false, + signers: None, + blockhash_query: BlockhashQuery::All, + nonce_account: None, + nonce_authority: None, + fee_payer: None, + from: None, }; process_command(&config).unwrap(); check_balance( @@ -1193,3 +1256,167 @@ fn test_stake_set_lockup() { server.close().unwrap(); remove_dir_all(ledger_path).unwrap(); } + +#[test] +fn test_offline_nonced_create_stake_account_and_withdraw() { + 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.keypair = keypair_from_seed(&[1u8; 32]).unwrap(); + config.json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); + + let mut config_offline = CliConfig::default(); + config_offline.keypair = keypair_from_seed(&[2u8; 32]).unwrap(); + let offline_pubkey = config_offline.keypair.pubkey(); + let (offline_keypair_file, mut tmp_file) = make_tmp_file(); + write_keypair(&config_offline.keypair, tmp_file.as_file_mut()).unwrap(); + config_offline.json_rpc_url = String::default(); + config_offline.command = CliCommand::ClusterVersion; + // Verfiy that we cannot reach the cluster + process_command(&config_offline).unwrap_err(); + + request_and_confirm_airdrop(&rpc_client, &faucet_addr, &config.keypair.pubkey(), 200_000) + .unwrap(); + check_balance(200_000, &rpc_client, &config.keypair.pubkey()); + + request_and_confirm_airdrop( + &rpc_client, + &faucet_addr, + &config_offline.keypair.pubkey(), + 100_000, + ) + .unwrap(); + check_balance(100_000, &rpc_client, &config_offline.keypair.pubkey()); + + // Create nonce account + let minimum_nonce_balance = rpc_client + .get_minimum_balance_for_rent_exemption(NonceState::size()) + .unwrap(); + let nonce_account = keypair_from_seed(&[3u8; 32]).unwrap(); + let nonce_pubkey = nonce_account.pubkey(); + 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(), + seed: None, + nonce_authority: Some(offline_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"), + }; + + // Create stake account offline + let stake_keypair = keypair_from_seed(&[4u8; 32]).unwrap(); + let stake_pubkey = stake_keypair.pubkey(); + let (stake_keypair_file, mut tmp_file) = make_tmp_file(); + write_keypair(&stake_keypair, tmp_file.as_file_mut()).unwrap(); + config_offline.command = CliCommand::CreateStakeAccount { + stake_account: read_keypair_file(&stake_keypair_file).unwrap().into(), + seed: None, + staker: None, + withdrawer: None, + lockup: Lockup::default(), + lamports: 50_000, + sign_only: true, + signers: None, + blockhash_query: BlockhashQuery::None(nonce_hash, FeeCalculator::default()), + nonce_account: Some(nonce_pubkey), + nonce_authority: None, + fee_payer: None, + from: None, + }; + let sig_response = process_command(&config_offline).unwrap(); + let (blockhash, signers) = parse_sign_only_reply_string(&sig_response); + config.command = CliCommand::CreateStakeAccount { + stake_account: stake_pubkey.into(), + seed: None, + staker: Some(offline_pubkey.into()), + withdrawer: None, + lockup: Lockup::default(), + lamports: 50_000, + sign_only: false, + signers: Some(signers), + blockhash_query: BlockhashQuery::FeeCalculator(blockhash), + nonce_account: Some(nonce_pubkey), + nonce_authority: Some(offline_pubkey.into()), + fee_payer: Some(offline_pubkey.into()), + from: Some(offline_pubkey.into()), + }; + process_command(&config).unwrap(); + check_balance(50_000, &rpc_client, &stake_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"), + }; + + // Offline, nonced stake-withdraw + let recipient = keypair_from_seed(&[5u8; 32]).unwrap(); + let recipient_pubkey = recipient.pubkey(); + config_offline.command = CliCommand::WithdrawStake { + stake_account_pubkey: stake_pubkey, + destination_account_pubkey: recipient_pubkey, + lamports: 42, + withdraw_authority: None, + sign_only: true, + signers: None, + blockhash_query: BlockhashQuery::None(nonce_hash, FeeCalculator::default()), + nonce_account: Some(nonce_pubkey), + nonce_authority: None, + fee_payer: None, + }; + let sig_response = process_command(&config_offline).unwrap(); + let (blockhash, signers) = parse_sign_only_reply_string(&sig_response); + config.command = CliCommand::WithdrawStake { + stake_account_pubkey: stake_pubkey, + destination_account_pubkey: recipient_pubkey, + lamports: 42, + withdraw_authority: Some(offline_pubkey.into()), + sign_only: false, + signers: Some(signers), + blockhash_query: BlockhashQuery::FeeCalculator(blockhash), + nonce_account: Some(nonce_pubkey), + nonce_authority: Some(offline_pubkey.into()), + fee_payer: Some(offline_pubkey.into()), + }; + process_command(&config).unwrap(); + check_balance(42, &rpc_client, &recipient_pubkey); + + // Test that offline derived addresses fail + config_offline.command = CliCommand::CreateStakeAccount { + stake_account: read_keypair_file(&stake_keypair_file).unwrap().into(), + seed: Some("fail".to_string()), + staker: None, + withdrawer: None, + lockup: Lockup::default(), + lamports: 50_000, + sign_only: true, + signers: None, + blockhash_query: BlockhashQuery::None(nonce_hash, FeeCalculator::default()), + nonce_account: Some(nonce_pubkey), + nonce_authority: Some(read_keypair_file(&offline_keypair_file).unwrap().into()), + fee_payer: Some(read_keypair_file(&offline_keypair_file).unwrap().into()), + from: Some(read_keypair_file(&offline_keypair_file).unwrap().into()), + }; + process_command(&config_offline).unwrap_err(); + + server.close().unwrap(); + remove_dir_all(ledger_path).unwrap(); +}