From 450f1d286705caea38121ccb66afdc669dece0af Mon Sep 17 00:00:00 2001 From: Greg Fitzgerald Date: Thu, 30 Apr 2020 17:56:37 -0600 Subject: [PATCH] Add set-lockup to solana-stake-accounts (#9827) * Add a command to set lockups or authorize a new custodian on derived stake accounts * Thanks clippy --- programs/stake/src/stake_state.rs | 12 +++++ stake-accounts/src/arg_parser.rs | 66 ++++++++++++++++++++++- stake-accounts/src/args.rs | 60 ++++++++++++++++++++- stake-accounts/src/main.rs | 33 +++++++++++- stake-accounts/src/stake_accounts.rs | 81 +++++++++++++++++++++++++++- 5 files changed, 246 insertions(+), 6 deletions(-) diff --git a/programs/stake/src/stake_state.rs b/programs/stake/src/stake_state.rs index 168607e1aa..e869debfef 100644 --- a/programs/stake/src/stake_state.rs +++ b/programs/stake/src/stake_state.rs @@ -77,6 +77,18 @@ impl StakeState { _ => None, } } + + pub fn lockup_from(account: &Account) -> Option { + Self::from(account).and_then(|state: Self| state.lockup()) + } + + pub fn lockup(&self) -> Option { + match self { + StakeState::Stake(meta, _stake) => Some(meta.lockup), + StakeState::Initialized(meta) => Some(meta.lockup), + _ => None, + } + } } #[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Copy)] diff --git a/stake-accounts/src/arg_parser.rs b/stake-accounts/src/arg_parser.rs index 89373bfd1b..a6bbe281cd 100644 --- a/stake-accounts/src/arg_parser.rs +++ b/stake-accounts/src/arg_parser.rs @@ -1,8 +1,12 @@ use crate::args::{ Args, AuthorizeArgs, Command, CountArgs, MoveArgs, NewArgs, QueryArgs, RebaseArgs, + SetLockupArgs, +}; +use clap::{value_t, value_t_or_exit, App, Arg, ArgMatches, SubCommand}; +use solana_clap_utils::{ + input_parsers::unix_timestamp_from_rfc3339_datetime, + input_validators::{is_amount, is_rfc3339_datetime, is_valid_pubkey, is_valid_signer}, }; -use clap::{value_t_or_exit, App, Arg, ArgMatches, SubCommand}; -use solana_clap_utils::input_validators::{is_amount, is_valid_pubkey, is_valid_signer}; use solana_cli_config::CONFIG_FILE; use solana_sdk::native_token::sol_to_lamports; use std::ffi::OsString; @@ -36,6 +40,23 @@ fn base_pubkey_arg<'a, 'b>() -> Arg<'a, 'b> { .help("Public key which stake account addresses are derived from") } +fn custodian_arg<'a, 'b>() -> Arg<'a, 'b> { + Arg::with_name("custodian") + .required(true) + .takes_value(true) + .value_name("KEYPAIR") + .validator(is_valid_signer) + .help("Authority to modify lockups") +} + +fn new_custodian_arg<'a, 'b>() -> Arg<'a, 'b> { + Arg::with_name("new_custodian") + .takes_value(true) + .value_name("PUBKEY") + .validator(is_valid_pubkey) + .help("New authority to modify lockups") +} + fn new_base_keypair_arg<'a, 'b>() -> Arg<'a, 'b> { Arg::with_name("new_base_keypair") .required(true) @@ -85,6 +106,23 @@ fn new_withdraw_authority_arg<'a, 'b>() -> Arg<'a, 'b> { .help("New withdraw authority") } +fn lockup_epoch_arg<'a, 'b>() -> Arg<'a, 'b> { + Arg::with_name("lockup_epoch") + .long("lockup-epoch") + .takes_value(true) + .value_name("NUMBER") + .help("The epoch height at which each account will be available for withdrawl") +} + +fn lockup_date_arg<'a, 'b>() -> Arg<'a, 'b> { + Arg::with_name("lockup_date") + .long("lockup-date") + .value_name("RFC3339 DATETIME") + .validator(is_rfc3339_datetime) + .takes_value(true) + .help("The date and time at which each account will be available for withdrawl") +} + fn num_accounts_arg<'a, 'b>() -> Arg<'a, 'b> { Arg::with_name("num_accounts") .long("num-accounts") @@ -197,6 +235,17 @@ where .arg(new_withdraw_authority_arg()) .arg(num_accounts_arg()), ) + .subcommand( + SubCommand::with_name("set-lockup") + .about("Set new lockups in all derived stake accounts") + .arg(fee_payer_arg()) + .arg(base_pubkey_arg().index(1)) + .arg(custodian_arg()) + .arg(lockup_epoch_arg()) + .arg(lockup_date_arg()) + .arg(new_custodian_arg()) + .arg(num_accounts_arg()), + ) .subcommand( SubCommand::with_name("rebase") .about("Relocate derived stake accounts") @@ -258,6 +307,18 @@ fn parse_authorize_args(matches: &ArgMatches<'_>) -> AuthorizeArgs) -> SetLockupArgs { + SetLockupArgs { + fee_payer: value_t_or_exit!(matches, "fee_payer", String), + base_pubkey: value_t_or_exit!(matches, "base_pubkey", String), + custodian: value_t_or_exit!(matches, "custodian", String), + lockup_epoch: value_t!(matches, "lockup_epoch", u64).ok(), + lockup_date: unix_timestamp_from_rfc3339_datetime(matches, "lockup_date"), + new_custodian: value_t!(matches, "new_custodian", String).ok(), + num_accounts: value_t_or_exit!(matches, "num_accounts", usize), + } +} + fn parse_rebase_args(matches: &ArgMatches<'_>) -> RebaseArgs { RebaseArgs { fee_payer: value_t_or_exit!(matches, "fee_payer", String), @@ -290,6 +351,7 @@ where ("addresses", Some(matches)) => Command::Addresses(parse_query_args(matches)), ("balance", Some(matches)) => Command::Balance(parse_query_args(matches)), ("authorize", Some(matches)) => Command::Authorize(parse_authorize_args(matches)), + ("set-lockup", Some(matches)) => Command::SetLockup(parse_set_lockup_args(matches)), ("rebase", Some(matches)) => Command::Rebase(parse_rebase_args(matches)), ("move", Some(matches)) => Command::Move(Box::new(parse_move_args(matches))), _ => { diff --git a/stake-accounts/src/args.rs b/stake-accounts/src/args.rs index dc0b116228..ee2cbc3eea 100644 --- a/stake-accounts/src/args.rs +++ b/stake-accounts/src/args.rs @@ -1,7 +1,11 @@ use clap::ArgMatches; use solana_clap_utils::keypair::{pubkey_from_path, signer_from_path}; use solana_remote_wallet::remote_wallet::RemoteWalletManager; -use solana_sdk::{pubkey::Pubkey, signature::Signer}; +use solana_sdk::{ + clock::{Epoch, UnixTimestamp}, + pubkey::Pubkey, + signature::Signer, +}; use std::error::Error; use std::sync::Arc; @@ -34,6 +38,16 @@ pub(crate) struct AuthorizeArgs { pub num_accounts: usize, } +pub(crate) struct SetLockupArgs { + pub fee_payer: K, + pub base_pubkey: P, + pub custodian: K, + pub lockup_epoch: Option, + pub lockup_date: Option, + pub new_custodian: Option

, + pub num_accounts: usize, +} + pub(crate) struct RebaseArgs { pub fee_payer: K, pub base_pubkey: P, @@ -53,6 +67,7 @@ pub(crate) enum Command { Addresses(QueryArgs

), Balance(QueryArgs

), Authorize(AuthorizeArgs), + SetLockup(SetLockupArgs), Rebase(RebaseArgs), Move(Box>), } @@ -103,6 +118,29 @@ fn resolve_fee_payer( signer_from_path(&matches, key_url, "fee-payer", wallet_manager) } +fn resolve_custodian( + wallet_manager: &mut Option>, + key_url: &str, +) -> Result, Box> { + let matches = ArgMatches::default(); + signer_from_path(&matches, key_url, "custodian", wallet_manager) +} + +fn resolve_new_custodian( + wallet_manager: &mut Option>, + key_url: &Option, +) -> Result, Box> { + let matches = ArgMatches::default(); + let pubkey = match key_url { + None => None, + Some(key_url) => { + let pubkey = pubkey_from_path(&matches, key_url, "new custodian", wallet_manager)?; + Some(pubkey) + } + }; + Ok(pubkey) +} + fn resolve_base_pubkey( wallet_manager: &mut Option>, key_url: &str, @@ -141,6 +179,22 @@ fn resolve_authorize_args( Ok(resolved_args) } +fn resolve_set_lockup_args( + wallet_manager: &mut Option>, + args: &SetLockupArgs, +) -> Result>, Box> { + let resolved_args = SetLockupArgs { + fee_payer: resolve_fee_payer(wallet_manager, &args.fee_payer)?, + base_pubkey: resolve_base_pubkey(wallet_manager, &args.base_pubkey)?, + custodian: resolve_custodian(wallet_manager, &args.custodian)?, + lockup_epoch: args.lockup_epoch, + lockup_date: args.lockup_date, + new_custodian: resolve_new_custodian(wallet_manager, &args.new_custodian)?, + num_accounts: args.num_accounts, + }; + Ok(resolved_args) +} + fn resolve_rebase_args( wallet_manager: &mut Option>, args: &RebaseArgs, @@ -217,6 +271,10 @@ pub(crate) fn resolve_command( let resolved_args = resolve_authorize_args(&mut wallet_manager, &args)?; Ok(Command::Authorize(resolved_args)) } + Command::SetLockup(args) => { + let resolved_args = resolve_set_lockup_args(&mut wallet_manager, &args)?; + Ok(Command::SetLockup(resolved_args)) + } Command::Rebase(args) => { let resolved_args = resolve_rebase_args(&mut wallet_manager, &args)?; Ok(Command::Rebase(resolved_args)) diff --git a/stake-accounts/src/main.rs b/stake-accounts/src/main.rs index a0070e440a..7546b0df2c 100644 --- a/stake-accounts/src/main.rs +++ b/stake-accounts/src/main.rs @@ -3,7 +3,9 @@ mod args; mod stake_accounts; use crate::arg_parser::parse_args; -use crate::args::{resolve_command, AuthorizeArgs, Command, MoveArgs, NewArgs, RebaseArgs}; +use crate::args::{ + resolve_command, AuthorizeArgs, Command, MoveArgs, NewArgs, RebaseArgs, SetLockupArgs, +}; use solana_cli_config::Config; use solana_client::client_error::ClientError; use solana_client::rpc_client::RpcClient; @@ -15,6 +17,7 @@ use solana_sdk::{ signers::Signers, transaction::Transaction, }; +use solana_stake_program::stake_instruction::LockupArgs; use std::env; use std::error::Error; @@ -53,6 +56,7 @@ fn process_new_stake_account( args.lamports, &args.stake_authority, &args.withdraw_authority, + &Pubkey::default(), args.index, ); let signers = vec![ @@ -89,6 +93,30 @@ fn process_authorize_stake_accounts( Ok(()) } +fn process_lockup_stake_accounts( + client: &RpcClient, + args: &SetLockupArgs>, +) -> Result<(), ClientError> { + let lockup = LockupArgs { + epoch: args.lockup_epoch, + unix_timestamp: args.lockup_date, + custodian: args.new_custodian, + }; + let messages = stake_accounts::lockup_stake_accounts( + &args.fee_payer.pubkey(), + &args.base_pubkey, + &args.custodian.pubkey(), + &lockup, + args.num_accounts, + ); + let signers = vec![&*args.fee_payer, &*args.custodian]; + for message in messages { + let signature = send_message(client, message, &signers)?; + println!("{}", signature); + } + Ok(()) +} + fn process_rebase_stake_accounts( client: &RpcClient, args: &RebaseArgs>, @@ -201,6 +229,9 @@ fn main() -> Result<(), Box> { Command::Authorize(args) => { process_authorize_stake_accounts(&client, &args)?; } + Command::SetLockup(args) => { + process_lockup_stake_accounts(&client, &args)?; + } Command::Rebase(args) => { process_rebase_stake_accounts(&client, &args)?; } diff --git a/stake-accounts/src/stake_accounts.rs b/stake-accounts/src/stake_accounts.rs index bebdf545e9..f2fc7502bb 100644 --- a/stake-accounts/src/stake_accounts.rs +++ b/stake-accounts/src/stake_accounts.rs @@ -1,6 +1,6 @@ use solana_sdk::{instruction::Instruction, message::Message, pubkey::Pubkey}; use solana_stake_program::{ - stake_instruction, + stake_instruction::{self, LockupArgs}, stake_state::{Authorized, Lockup, StakeAuthorize}, }; @@ -25,6 +25,7 @@ pub(crate) fn new_stake_account( lamports: u64, stake_authority_pubkey: &Pubkey, withdraw_authority_pubkey: &Pubkey, + custodian_pubkey: &Pubkey, index: usize, ) -> Message { let stake_account_address = derive_stake_account_address(base_pubkey, index); @@ -32,13 +33,17 @@ pub(crate) fn new_stake_account( staker: *stake_authority_pubkey, withdrawer: *withdraw_authority_pubkey, }; + let lockup = Lockup { + custodian: *custodian_pubkey, + ..Lockup::default() + }; let instructions = stake_instruction::create_account_with_seed( funding_pubkey, &stake_account_address, &base_pubkey, &index.to_string(), &authorized, - &Lockup::default(), + &lockup, lamports, ); Message::new_with_payer(&instructions, Some(fee_payer_pubkey)) @@ -152,6 +157,23 @@ pub(crate) fn authorize_stake_accounts( .collect::>() } +pub(crate) fn lockup_stake_accounts( + fee_payer_pubkey: &Pubkey, + base_pubkey: &Pubkey, + custodian_pubkey: &Pubkey, + lockup: &LockupArgs, + num_accounts: usize, +) -> Vec { + let stake_account_addresses = derive_stake_account_addresses(base_pubkey, num_accounts); + stake_account_addresses + .iter() + .map(|address| { + let instruction = stake_instruction::set_lockup(address, &lockup, custodian_pubkey); + Message::new_with_payer(&[instruction], Some(&fee_payer_pubkey)) + }) + .collect() +} + pub(crate) fn rebase_stake_accounts( fee_payer_pubkey: &Pubkey, new_base_pubkey: &Pubkey, @@ -273,6 +295,7 @@ mod tests { lamports, &stake_authority_pubkey, &withdraw_authority_pubkey, + &Pubkey::default(), 0, ); @@ -310,6 +333,7 @@ mod tests { lamports, &stake_authority_pubkey, &withdraw_authority_pubkey, + &Pubkey::default(), 0, ); @@ -343,6 +367,57 @@ mod tests { assert_eq!(authorized.withdrawer, new_withdraw_authority_pubkey); } + #[test] + fn test_lockup_stake_accounts() { + let (bank, funding_keypair, rent) = create_bank(10_000_000); + let funding_pubkey = funding_keypair.pubkey(); + let bank_client = BankClient::new(bank); + let fee_payer_keypair = create_account(&bank_client, &funding_keypair, 1); + let fee_payer_pubkey = fee_payer_keypair.pubkey(); + + let base_keypair = Keypair::new(); + let base_pubkey = base_keypair.pubkey(); + let lamports = rent + 1; + + let custodian_keypair = Keypair::new(); + let custodian_pubkey = custodian_keypair.pubkey(); + + let message = new_stake_account( + &fee_payer_pubkey, + &funding_pubkey, + &base_pubkey, + lamports, + &Pubkey::default(), + &Pubkey::default(), + &custodian_pubkey, + 0, + ); + + let signers = [&funding_keypair, &fee_payer_keypair, &base_keypair]; + bank_client.send_message(&signers, message).unwrap(); + + let messages = lockup_stake_accounts( + &fee_payer_pubkey, + &base_pubkey, + &custodian_pubkey, + &LockupArgs { + unix_timestamp: Some(1), + ..LockupArgs::default() + }, + 1, + ); + + let signers = [&fee_payer_keypair, &custodian_keypair]; + for message in messages { + bank_client.send_message(&signers, message).unwrap(); + } + + let account = get_account_at(&bank_client, &base_pubkey, 0); + let lockup = StakeState::lockup_from(&account).unwrap(); + assert_eq!(lockup.unix_timestamp, 1); + assert_eq!(lockup.epoch, 0); + } + #[test] fn test_rebase_empty_account() { let pubkey = Pubkey::default(); @@ -384,6 +459,7 @@ mod tests { lamports, &stake_authority_pubkey, &withdraw_authority_pubkey, + &Pubkey::default(), 0, ); @@ -442,6 +518,7 @@ mod tests { lamports, &stake_authority_pubkey, &withdraw_authority_pubkey, + &Pubkey::default(), 0, );