diff --git a/Cargo.lock b/Cargo.lock index 14c10247f3..abd4e4f1fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4584,6 +4584,20 @@ dependencies = [ "syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "solana-stake-accounts" +version = "1.2.0" +dependencies = [ + "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", + "solana-clap-utils 1.2.0", + "solana-cli-config 1.2.0", + "solana-client 1.2.0", + "solana-remote-wallet 1.2.0", + "solana-runtime 1.2.0", + "solana-sdk 1.2.0", + "solana-stake-program 1.2.0", +] + [[package]] name = "solana-stake-monitor" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index d8caa70e88..57f0164520 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,7 @@ members = [ "sdk", "sdk-c", "scripts", + "stake-accounts", "stake-monitor", "sys-tuner", "transaction-status", diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index ff49209314..fe064c7d46 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -15,6 +15,7 @@ * [Generate Keys](cli/generate-keys.md) * [Send and Receive Tokens](cli/transfer-tokens.md) * [Delegate Stake](cli/delegate-stake.md) + * [Manage Stake Accounts](cli/manage-stake-accounts.md) * [Offline Signing](offline-signing/README.md) * [Durable Transaction Nonces](offline-signing/durable-nonce.md) * [Command-line Reference](cli/usage.md) diff --git a/docs/src/cli/manage-stake-accounts.md b/docs/src/cli/manage-stake-accounts.md new file mode 100644 index 0000000000..2d3b3d95b9 --- /dev/null +++ b/docs/src/cli/manage-stake-accounts.md @@ -0,0 +1,74 @@ +# Manage Stake Accounts + +If you want to delegate stake to many different validators, you will need +to create a separate stake account for each. If you follow the convention +of creating the first stake account at seed "0", the second at "1", the +third at "2", and so on, then the `solana-stake-accounts` tool will allow +you to operate on all accounts with single invocations. You can use it to +sum up the balances of all accounts, move accounts to a new wallet, or set +new authorities. + +## Usage + +### Create a stake account + +Create and fund a derived stake account at the stake authority public key: + +```bash +solana-stake-accounts new \ + --stake-authority --withdraw-authority +``` + +### Count accounts + +Count the number of derived accounts: + +```bash +solana-stake-accounts count +``` + +### Get stake account balances + +Sum the balance of derived stake accounts: + +```bash +solana-stake-accounts balance --num-accounts +``` + +### Get stake account addresses + +List the address of each stake account derived from the given public key: + +```bash +solana-stake-accounts addresses --num-accounts +``` + +### Set new authorities + +Set new authorities on each derived stake account: + +```bash +solana-stake-accounts authorize \ + --stake-authority --withdraw-authority \ + --new-stake-authority --new-withdraw-authority \ + --num-accounts +``` + +### Relocate stake accounts + +Relocate stake accounts: + +```bash +solana-stake-accounts rebase \ + --stake-authority --num-accounts +``` + +To atomically rebase and authorize each stake account, use the 'move' +command: + +```bash +solana-stake-accounts move \ + --stake-authority --withdraw-authority \ + --new-stake-authority --new-withdraw-authority \ + --num-accounts +``` diff --git a/stake-accounts/Cargo.toml b/stake-accounts/Cargo.toml new file mode 100644 index 0000000000..9bda62aa7a --- /dev/null +++ b/stake-accounts/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "solana-stake-accounts" +description = "Blockchain, Rebuilt for Scale" +authors = ["Solana Maintainers "] +edition = "2018" +version = "1.1.0" +repository = "https://github.com/solana-labs/solana" +license = "Apache-2.0" +homepage = "https://solana.com/" + +[dependencies] +clap = "2.33.0" +solana-clap-utils = { path = "../clap-utils", version = "1.1.0" } +solana-cli-config = { path = "../cli-config", version = "1.1.0" } +solana-client = { path = "../client", version = "1.1.0" } +solana-remote-wallet = { path = "../remote-wallet", version = "1.1.0" } +solana-sdk = { path = "../sdk", version = "1.1.0" } +solana-stake-program = { path = "../programs/stake", version = "1.1.0" } + +[dev-dependencies] +solana-runtime = { path = "../runtime", version = "1.1.0" } diff --git a/stake-accounts/src/args.rs b/stake-accounts/src/args.rs new file mode 100644 index 0000000000..3d2932e4a0 --- /dev/null +++ b/stake-accounts/src/args.rs @@ -0,0 +1,382 @@ +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; +use std::process::exit; + +pub(crate) struct NewCommandConfig { + pub fee_payer: String, + pub funding_keypair: String, + pub base_keypair: String, + pub lamports: u64, + pub stake_authority: String, + pub withdraw_authority: String, + pub index: usize, +} + +pub(crate) struct CountCommandConfig { + pub base_pubkey: String, +} + +pub(crate) struct QueryCommandConfig { + pub base_pubkey: String, + pub num_accounts: usize, +} + +pub(crate) struct AuthorizeCommandConfig { + pub fee_payer: String, + pub base_pubkey: String, + pub stake_authority: String, + pub withdraw_authority: String, + pub new_stake_authority: String, + pub new_withdraw_authority: String, + pub num_accounts: usize, +} + +pub(crate) struct RebaseCommandConfig { + pub fee_payer: String, + pub base_pubkey: String, + pub new_base_keypair: String, + pub stake_authority: String, + pub num_accounts: usize, +} + +pub(crate) struct MoveCommandConfig { + pub rebase_config: RebaseCommandConfig, + pub authorize_config: AuthorizeCommandConfig, +} + +pub(crate) enum Command { + New(NewCommandConfig), + Count(CountCommandConfig), + Addresses(QueryCommandConfig), + Balance(QueryCommandConfig), + Authorize(AuthorizeCommandConfig), + Rebase(RebaseCommandConfig), + Move(Box), +} + +pub(crate) struct CommandConfig { + pub config_file: String, + pub url: Option, + pub command: Command, +} + +fn fee_payer_arg<'a, 'b>() -> Arg<'a, 'b> { + Arg::with_name("fee_payer") + .long("fee-payer") + .required(true) + .takes_value(true) + .value_name("KEYPAIR") + .validator(is_valid_signer) + .help("Fee payer") +} + +fn funding_keypair_arg<'a, 'b>() -> Arg<'a, 'b> { + Arg::with_name("funding_keypair") + .required(true) + .takes_value(true) + .value_name("FUNDING_KEYPAIR") + .validator(is_valid_signer) + .help("Keypair to fund accounts") +} + +fn base_pubkey_arg<'a, 'b>() -> Arg<'a, 'b> { + Arg::with_name("base_pubkey") + .required(true) + .takes_value(true) + .value_name("BASE_PUBKEY") + .validator(is_valid_pubkey) + .help("Public key which stake account addresses are derived from") +} + +fn new_base_keypair_arg<'a, 'b>() -> Arg<'a, 'b> { + Arg::with_name("new_base_keypair") + .required(true) + .takes_value(true) + .value_name("NEW_BASE_KEYPAIR") + .validator(is_valid_signer) + .help("New keypair which stake account addresses are derived from") +} + +fn stake_authority_arg<'a, 'b>() -> Arg<'a, 'b> { + Arg::with_name("stake_authority") + .long("stake-authority") + .required(true) + .takes_value(true) + .value_name("KEYPAIR") + .validator(is_valid_signer) + .help("Stake authority") +} + +fn withdraw_authority_arg<'a, 'b>() -> Arg<'a, 'b> { + Arg::with_name("withdraw_authority") + .long("withdraw-authority") + .required(true) + .takes_value(true) + .value_name("KEYPAIR") + .validator(is_valid_signer) + .help("Withdraw authority") +} + +fn new_stake_authority_arg<'a, 'b>() -> Arg<'a, 'b> { + Arg::with_name("new_stake_authority") + .long("new-stake-authority") + .required(true) + .takes_value(true) + .value_name("PUBKEY") + .validator(is_valid_pubkey) + .help("New stake authority") +} + +fn new_withdraw_authority_arg<'a, 'b>() -> Arg<'a, 'b> { + Arg::with_name("new_withdraw_authority") + .long("new-withdraw-authority") + .required(true) + .takes_value(true) + .value_name("PUBKEY") + .validator(is_valid_pubkey) + .help("New withdraw authority") +} + +fn num_accounts_arg<'a, 'b>() -> Arg<'a, 'b> { + Arg::with_name("num_accounts") + .long("num-accounts") + .required(true) + .takes_value(true) + .value_name("NUMBER") + .help("Number of derived stake accounts") +} + +pub(crate) fn get_matches<'a, I, T>(args: I) -> ArgMatches<'a> +where + I: IntoIterator, + T: Into + Clone, +{ + let default_config_file = CONFIG_FILE.as_ref().unwrap(); + App::new("solana-stake-accounts") + .about("about") + .version("version") + .arg( + Arg::with_name("config_file") + .long("config") + .takes_value(true) + .value_name("FILEPATH") + .default_value(default_config_file) + .help("Config file"), + ) + .arg( + Arg::with_name("url") + .long("url") + .global(true) + .takes_value(true) + .value_name("URL") + .help("RPC entrypoint address. i.e. http://devnet.solana.com"), + ) + .subcommand( + SubCommand::with_name("new") + .about("Create derived stake accounts") + .arg(fee_payer_arg()) + .arg(funding_keypair_arg().index(1)) + .arg( + Arg::with_name("base_keypair") + .required(true) + .index(2) + .takes_value(true) + .value_name("BASE_KEYPAIR") + .validator(is_valid_signer) + .help("Keypair which stake account addresses are derived from"), + ) + .arg( + Arg::with_name("amount") + .required(true) + .index(3) + .takes_value(true) + .value_name("AMOUNT") + .validator(is_amount) + .help("Amount to move into the new stake accounts, in SOL"), + ) + .arg( + Arg::with_name("stake_authority") + .long("stake-authority") + .required(true) + .takes_value(true) + .value_name("PUBKEY") + .validator(is_valid_pubkey) + .help("Stake authority"), + ) + .arg( + Arg::with_name("withdraw_authority") + .long("withdraw-authority") + .required(true) + .takes_value(true) + .value_name("PUBKEY") + .validator(is_valid_pubkey) + .help("Withdraw authority"), + ) + .arg( + Arg::with_name("index") + .long("index") + .takes_value(true) + .default_value("0") + .value_name("NUMBER") + .help("Index of the derived account to create"), + ), + ) + .subcommand( + SubCommand::with_name("count") + .about("Count derived stake accounts") + .arg(base_pubkey_arg().index(1)), + ) + .subcommand( + SubCommand::with_name("addresses") + .about("Show public keys of all derived stake accounts") + .arg(base_pubkey_arg().index(1)) + .arg(num_accounts_arg()), + ) + .subcommand( + SubCommand::with_name("balance") + .about("Sum balances of all derived stake accounts") + .arg(base_pubkey_arg().index(1)) + .arg(num_accounts_arg()), + ) + .subcommand( + SubCommand::with_name("authorize") + .about("Set new authorities in all derived stake accounts") + .arg(fee_payer_arg()) + .arg(base_pubkey_arg().index(1)) + .arg(stake_authority_arg()) + .arg(withdraw_authority_arg()) + .arg(new_stake_authority_arg()) + .arg(new_withdraw_authority_arg()) + .arg(num_accounts_arg()), + ) + .subcommand( + SubCommand::with_name("rebase") + .about("Relocate derived stake accounts") + .arg(fee_payer_arg()) + .arg(base_pubkey_arg().index(1)) + .arg(new_base_keypair_arg().index(2)) + .arg(stake_authority_arg()) + .arg(num_accounts_arg()), + ) + .subcommand( + SubCommand::with_name("move") + .about("Rebase and set new authorities in all derived stake accounts") + .arg(fee_payer_arg()) + .arg(base_pubkey_arg().index(1)) + .arg(new_base_keypair_arg().index(2)) + .arg(stake_authority_arg()) + .arg(withdraw_authority_arg()) + .arg(new_stake_authority_arg()) + .arg(new_withdraw_authority_arg()) + .arg(num_accounts_arg()), + ) + .get_matches_from(args) +} + +fn parse_new_args(matches: &ArgMatches<'_>) -> NewCommandConfig { + let fee_payer = value_t_or_exit!(matches, "fee_payer", String); + let funding_keypair = value_t_or_exit!(matches, "funding_keypair", String); + let lamports = sol_to_lamports(value_t_or_exit!(matches, "amount", f64)); + let base_keypair = value_t_or_exit!(matches, "base_keypair", String); + let stake_authority = value_t_or_exit!(matches, "stake_authority", String); + let withdraw_authority = value_t_or_exit!(matches, "withdraw_authority", String); + let index = value_t_or_exit!(matches, "index", usize); + NewCommandConfig { + fee_payer, + funding_keypair, + lamports, + base_keypair, + stake_authority, + withdraw_authority, + index, + } +} + +fn parse_count_args(matches: &ArgMatches<'_>) -> CountCommandConfig { + let base_pubkey = value_t_or_exit!(matches, "base_pubkey", String); + CountCommandConfig { base_pubkey } +} + +fn parse_query_args(matches: &ArgMatches<'_>) -> QueryCommandConfig { + let base_pubkey = value_t_or_exit!(matches, "base_pubkey", String); + let num_accounts = value_t_or_exit!(matches, "num_accounts", usize); + QueryCommandConfig { + base_pubkey, + num_accounts, + } +} + +fn parse_authorize_args(matches: &ArgMatches<'_>) -> AuthorizeCommandConfig { + let fee_payer = value_t_or_exit!(matches, "fee_payer", String); + let base_pubkey = value_t_or_exit!(matches, "base_pubkey", String); + let stake_authority = value_t_or_exit!(matches, "stake_authority", String); + let withdraw_authority = value_t_or_exit!(matches, "withdraw_authority", String); + let new_stake_authority = value_t_or_exit!(matches, "new_stake_authority", String); + let new_withdraw_authority = value_t_or_exit!(matches, "new_withdraw_authority", String); + let num_accounts = value_t_or_exit!(matches, "num_accounts", usize); + AuthorizeCommandConfig { + fee_payer, + base_pubkey, + stake_authority, + withdraw_authority, + new_stake_authority, + new_withdraw_authority, + num_accounts, + } +} + +fn parse_rebase_args(matches: &ArgMatches<'_>) -> RebaseCommandConfig { + let fee_payer = value_t_or_exit!(matches, "fee_payer", String); + let base_pubkey = value_t_or_exit!(matches, "base_pubkey", String); + let new_base_keypair = value_t_or_exit!(matches, "new_base_keypair", String); + let stake_authority = value_t_or_exit!(matches, "stake_authority", String); + let num_accounts = value_t_or_exit!(matches, "num_accounts", usize); + RebaseCommandConfig { + fee_payer, + base_pubkey, + new_base_keypair, + stake_authority, + num_accounts, + } +} + +fn parse_move_args(matches: &ArgMatches<'_>) -> MoveCommandConfig { + let rebase_config = parse_rebase_args(matches); + let authorize_config = parse_authorize_args(matches); + MoveCommandConfig { + rebase_config, + authorize_config, + } +} + +pub(crate) fn parse_args(args: I) -> CommandConfig +where + I: IntoIterator, + T: Into + Clone, +{ + let matches = get_matches(args); + let config_file = matches.value_of("config_file").unwrap().to_string(); + let url = matches.value_of("url").map(|x| x.to_string()); + + let command = match matches.subcommand() { + ("new", Some(matches)) => Command::New(parse_new_args(matches)), + ("count", Some(matches)) => Command::Count(parse_count_args(matches)), + ("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)), + ("rebase", Some(matches)) => Command::Rebase(parse_rebase_args(matches)), + ("move", Some(matches)) => Command::Move(Box::new(parse_move_args(matches))), + _ => { + eprintln!("{}", matches.usage()); + exit(1); + } + }; + CommandConfig { + config_file, + url, + command, + } +} diff --git a/stake-accounts/src/main.rs b/stake-accounts/src/main.rs new file mode 100644 index 0000000000..3ce97d0ce5 --- /dev/null +++ b/stake-accounts/src/main.rs @@ -0,0 +1,306 @@ +mod args; +mod stake_accounts; + +use crate::args::{ + parse_args, AuthorizeCommandConfig, Command, MoveCommandConfig, NewCommandConfig, + RebaseCommandConfig, +}; +use clap::ArgMatches; +use solana_clap_utils::keypair::{pubkey_from_path, signer_from_path}; +use solana_cli_config::Config; +use solana_client::client_error::ClientError; +use solana_client::rpc_client::RpcClient; +use solana_remote_wallet::remote_wallet::{maybe_wallet_manager, RemoteWalletManager}; +use solana_sdk::{ + message::Message, + native_token::lamports_to_sol, + pubkey::Pubkey, + signature::{Signature, Signer}, + signers::Signers, + transaction::Transaction, +}; +use std::env; +use std::error::Error; +use std::sync::Arc; + +fn resolve_stake_authority( + wallet_manager: Option<&Arc>, + key_url: &str, +) -> Result, Box> { + let matches = ArgMatches::default(); + signer_from_path(&matches, key_url, "stake authority", wallet_manager) +} + +fn resolve_withdraw_authority( + wallet_manager: Option<&Arc>, + key_url: &str, +) -> Result, Box> { + let matches = ArgMatches::default(); + signer_from_path(&matches, key_url, "withdraw authority", wallet_manager) +} + +fn resolve_new_stake_authority( + wallet_manager: Option<&Arc>, + key_url: &str, +) -> Result> { + let matches = ArgMatches::default(); + pubkey_from_path(&matches, key_url, "new stake authority", wallet_manager) +} + +fn resolve_new_withdraw_authority( + wallet_manager: Option<&Arc>, + key_url: &str, +) -> Result> { + let matches = ArgMatches::default(); + pubkey_from_path(&matches, key_url, "new withdraw authority", wallet_manager) +} + +fn resolve_fee_payer( + wallet_manager: Option<&Arc>, + key_url: &str, +) -> Result, Box> { + let matches = ArgMatches::default(); + signer_from_path(&matches, key_url, "fee-payer", wallet_manager) +} + +fn resolve_base_pubkey( + wallet_manager: Option<&Arc>, + key_url: &str, +) -> Result> { + let matches = ArgMatches::default(); + pubkey_from_path(&matches, key_url, "base pubkey", wallet_manager) +} + +fn get_balance_at(client: &RpcClient, pubkey: &Pubkey, i: usize) -> Result { + let address = stake_accounts::derive_stake_account_address(pubkey, i); + client.get_balance(&address) +} + +// Return the number of derived stake accounts with balances +fn count_stake_accounts(client: &RpcClient, base_pubkey: &Pubkey) -> Result { + let mut i = 0; + while get_balance_at(client, base_pubkey, i)? > 0 { + i += 1; + } + Ok(i) +} + +fn get_balances( + client: &RpcClient, + addresses: Vec, +) -> Result, ClientError> { + addresses + .into_iter() + .map(|pubkey| client.get_balance(&pubkey).map(|bal| (pubkey, bal))) + .collect() +} + +fn process_new_stake_account( + client: &RpcClient, + wallet_manager: Option<&Arc>, + new_config: &NewCommandConfig, +) -> Result> { + let matches = ArgMatches::default(); + let fee_payer_keypair = resolve_fee_payer(wallet_manager, &new_config.fee_payer)?; + let funding_keypair = signer_from_path( + &matches, + &new_config.funding_keypair, + "funding keypair", + wallet_manager, + )?; + let base_keypair = signer_from_path( + &matches, + &new_config.base_keypair, + "base keypair", + wallet_manager, + )?; + let stake_authority_pubkey = pubkey_from_path( + &matches, + &new_config.stake_authority, + "stake authority", + wallet_manager, + )?; + let withdraw_authority_pubkey = pubkey_from_path( + &matches, + &new_config.withdraw_authority, + "withdraw authority", + wallet_manager, + )?; + let message = stake_accounts::new_stake_account( + &fee_payer_keypair.pubkey(), + &funding_keypair.pubkey(), + &base_keypair.pubkey(), + new_config.lamports, + &stake_authority_pubkey, + &withdraw_authority_pubkey, + new_config.index, + ); + let signers = vec![&*fee_payer_keypair, &*funding_keypair, &*base_keypair]; + let signature = send_message(client, message, &signers)?; + Ok(signature) +} + +fn process_authorize_stake_accounts( + client: &RpcClient, + wallet_manager: Option<&Arc>, + authorize_config: &AuthorizeCommandConfig, +) -> Result<(), Box> { + let fee_payer_keypair = resolve_fee_payer(wallet_manager, &authorize_config.fee_payer)?; + let base_pubkey = resolve_base_pubkey(wallet_manager, &authorize_config.base_pubkey)?; + let stake_authority_keypair = + resolve_stake_authority(wallet_manager, &authorize_config.stake_authority)?; + let withdraw_authority_keypair = + resolve_withdraw_authority(wallet_manager, &authorize_config.withdraw_authority)?; + let new_stake_authority_pubkey = + resolve_new_stake_authority(wallet_manager, &authorize_config.new_stake_authority)?; + let new_withdraw_authority_pubkey = + resolve_new_withdraw_authority(wallet_manager, &authorize_config.new_withdraw_authority)?; + let messages = stake_accounts::authorize_stake_accounts( + &fee_payer_keypair.pubkey(), + &base_pubkey, + &stake_authority_keypair.pubkey(), + &withdraw_authority_keypair.pubkey(), + &new_stake_authority_pubkey, + &new_withdraw_authority_pubkey, + authorize_config.num_accounts, + ); + let signers = vec![ + &*fee_payer_keypair, + &*stake_authority_keypair, + &*withdraw_authority_keypair, + ]; + for message in messages { + let signature = send_message(client, message, &signers)?; + println!("{}", signature); + } + Ok(()) +} + +fn process_rebase_stake_accounts( + client: &RpcClient, + wallet_manager: Option<&Arc>, + rebase_config: &RebaseCommandConfig, +) -> Result<(), Box> { + let fee_payer_keypair = resolve_fee_payer(wallet_manager, &rebase_config.fee_payer)?; + let base_pubkey = resolve_base_pubkey(wallet_manager, &rebase_config.base_pubkey)?; + let stake_authority_keypair = + resolve_stake_authority(wallet_manager, &rebase_config.stake_authority)?; + let addresses = + stake_accounts::derive_stake_account_addresses(&base_pubkey, rebase_config.num_accounts); + let balances = get_balances(&client, addresses)?; + + let messages = stake_accounts::rebase_stake_accounts( + &fee_payer_keypair.pubkey(), + &base_pubkey, + &stake_authority_keypair.pubkey(), + &balances, + ); + let signers = vec![&*fee_payer_keypair, &*stake_authority_keypair]; + for message in messages { + let signature = send_message(client, message, &signers)?; + println!("{}", signature); + } + Ok(()) +} + +fn process_move_stake_accounts( + client: &RpcClient, + wallet_manager: Option<&Arc>, + move_config: &MoveCommandConfig, +) -> Result<(), Box> { + let authorize_config = &move_config.authorize_config; + let fee_payer_keypair = resolve_fee_payer(wallet_manager, &authorize_config.fee_payer)?; + let base_pubkey = resolve_base_pubkey(wallet_manager, &authorize_config.base_pubkey)?; + let stake_authority_keypair = + resolve_stake_authority(wallet_manager, &authorize_config.stake_authority)?; + let withdraw_authority_keypair = + resolve_withdraw_authority(wallet_manager, &authorize_config.withdraw_authority)?; + let new_stake_authority_pubkey = + resolve_new_stake_authority(wallet_manager, &authorize_config.new_stake_authority)?; + let new_withdraw_authority_pubkey = + resolve_new_withdraw_authority(wallet_manager, &authorize_config.new_withdraw_authority)?; + let addresses = + stake_accounts::derive_stake_account_addresses(&base_pubkey, authorize_config.num_accounts); + let balances = get_balances(&client, addresses)?; + + let messages = stake_accounts::move_stake_accounts( + &fee_payer_keypair.pubkey(), + &base_pubkey, + &stake_authority_keypair.pubkey(), + &withdraw_authority_keypair.pubkey(), + &new_stake_authority_pubkey, + &new_withdraw_authority_pubkey, + &balances, + ); + let signers = vec![ + &*fee_payer_keypair, + &*stake_authority_keypair, + &*withdraw_authority_keypair, + ]; + for message in messages { + let signature = send_message(client, message, &signers)?; + println!("{}", signature); + } + Ok(()) +} + +fn send_message( + client: &RpcClient, + message: Message, + signers: &S, +) -> Result { + let mut transaction = Transaction::new_unsigned(message); + client.resign_transaction(&mut transaction, signers)?; + client.send_and_confirm_transaction_with_spinner(&mut transaction, signers) +} + +fn main() -> Result<(), Box> { + let command_config = parse_args(env::args_os()); + let config = Config::load(&command_config.config_file)?; + let json_rpc_url = command_config.url.unwrap_or(config.json_rpc_url); + let client = RpcClient::new(json_rpc_url); + + let wallet_manager = maybe_wallet_manager()?; + let wallet_manager = wallet_manager.as_ref(); + match command_config.command { + Command::New(new_config) => { + process_new_stake_account(&client, wallet_manager, &new_config)?; + } + Command::Count(count_config) => { + let base_pubkey = resolve_base_pubkey(wallet_manager, &count_config.base_pubkey)?; + let num_accounts = count_stake_accounts(&client, &base_pubkey)?; + println!("{}", num_accounts); + } + Command::Addresses(query_config) => { + let base_pubkey = resolve_base_pubkey(wallet_manager, &query_config.base_pubkey)?; + let addresses = stake_accounts::derive_stake_account_addresses( + &base_pubkey, + query_config.num_accounts, + ); + for address in addresses { + println!("{:?}", address); + } + } + Command::Balance(query_config) => { + let base_pubkey = resolve_base_pubkey(wallet_manager, &query_config.base_pubkey)?; + let addresses = stake_accounts::derive_stake_account_addresses( + &base_pubkey, + query_config.num_accounts, + ); + let balances = get_balances(&client, addresses)?; + let lamports: u64 = balances.into_iter().map(|(_, bal)| bal).sum(); + let sol = lamports_to_sol(lamports); + println!("{} SOL", sol); + } + Command::Authorize(authorize_config) => { + process_authorize_stake_accounts(&client, wallet_manager, &authorize_config)?; + } + Command::Rebase(rebase_config) => { + process_rebase_stake_accounts(&client, wallet_manager, &rebase_config)?; + } + Command::Move(move_config) => { + process_move_stake_accounts(&client, wallet_manager, &move_config)?; + } + } + Ok(()) +} diff --git a/stake-accounts/src/stake_accounts.rs b/stake-accounts/src/stake_accounts.rs new file mode 100644 index 0000000000..f62289c460 --- /dev/null +++ b/stake-accounts/src/stake_accounts.rs @@ -0,0 +1,463 @@ +use solana_sdk::{instruction::Instruction, message::Message, pubkey::Pubkey}; +use solana_stake_program::{ + stake_instruction, + stake_state::{Authorized, Lockup, StakeAuthorize}, +}; + +pub(crate) fn derive_stake_account_address(base_pubkey: &Pubkey, i: usize) -> Pubkey { + Pubkey::create_with_seed(base_pubkey, &i.to_string(), &solana_stake_program::id()).unwrap() +} + +// Return derived addresses +pub(crate) fn derive_stake_account_addresses( + base_pubkey: &Pubkey, + num_accounts: usize, +) -> Vec { + (0..num_accounts) + .map(|i| derive_stake_account_address(base_pubkey, i)) + .collect() +} + +pub(crate) fn new_stake_account( + fee_payer_pubkey: &Pubkey, + funding_pubkey: &Pubkey, + base_pubkey: &Pubkey, + lamports: u64, + stake_authority_pubkey: &Pubkey, + withdraw_authority_pubkey: &Pubkey, + index: usize, +) -> Message { + let stake_account_address = derive_stake_account_address(base_pubkey, index); + let authorized = Authorized { + staker: *stake_authority_pubkey, + withdrawer: *withdraw_authority_pubkey, + }; + let instructions = stake_instruction::create_account_with_seed( + funding_pubkey, + &stake_account_address, + &base_pubkey, + &index.to_string(), + &authorized, + &Lockup::default(), + lamports, + ); + Message::new_with_payer(&instructions, Some(fee_payer_pubkey)) +} + +fn authorize_stake_accounts_instructions( + stake_account_address: &Pubkey, + stake_authority_pubkey: &Pubkey, + withdraw_authority_pubkey: &Pubkey, + new_stake_authority_pubkey: &Pubkey, + new_withdraw_authority_pubkey: &Pubkey, +) -> Vec { + let instruction0 = stake_instruction::authorize( + &stake_account_address, + stake_authority_pubkey, + new_stake_authority_pubkey, + StakeAuthorize::Staker, + ); + let instruction1 = stake_instruction::authorize( + &stake_account_address, + withdraw_authority_pubkey, + new_withdraw_authority_pubkey, + StakeAuthorize::Withdrawer, + ); + vec![instruction0, instruction1] +} + +fn rebase_stake_account( + stake_account_address: &Pubkey, + new_base_pubkey: &Pubkey, + i: usize, + fee_payer_pubkey: &Pubkey, + stake_authority_pubkey: &Pubkey, + lamports: u64, +) -> Message { + let new_stake_account_address = derive_stake_account_address(new_base_pubkey, i); + let instructions = stake_instruction::split_with_seed( + stake_account_address, + stake_authority_pubkey, + lamports, + &new_stake_account_address, + new_base_pubkey, + &i.to_string(), + ); + Message::new_with_payer(&instructions, Some(&fee_payer_pubkey)) +} + +fn move_stake_account( + stake_account_address: &Pubkey, + new_base_pubkey: &Pubkey, + i: usize, + fee_payer_pubkey: &Pubkey, + stake_authority_pubkey: &Pubkey, + withdraw_authority_pubkey: &Pubkey, + new_stake_authority_pubkey: &Pubkey, + new_withdraw_authority_pubkey: &Pubkey, + lamports: u64, +) -> Message { + let new_stake_account_address = derive_stake_account_address(new_base_pubkey, i); + let mut instructions = stake_instruction::split_with_seed( + stake_account_address, + stake_authority_pubkey, + lamports, + &new_stake_account_address, + new_base_pubkey, + &i.to_string(), + ); + + let authorize_instructions = authorize_stake_accounts_instructions( + &new_stake_account_address, + stake_authority_pubkey, + withdraw_authority_pubkey, + new_stake_authority_pubkey, + new_withdraw_authority_pubkey, + ); + + instructions.extend(authorize_instructions.into_iter()); + Message::new_with_payer(&instructions, Some(&fee_payer_pubkey)) +} + +pub(crate) fn authorize_stake_accounts( + fee_payer_pubkey: &Pubkey, + base_pubkey: &Pubkey, + stake_authority_pubkey: &Pubkey, + withdraw_authority_pubkey: &Pubkey, + new_stake_authority_pubkey: &Pubkey, + new_withdraw_authority_pubkey: &Pubkey, + num_accounts: usize, +) -> Vec { + let stake_account_addresses = derive_stake_account_addresses(base_pubkey, num_accounts); + stake_account_addresses + .iter() + .map(|stake_account_address| { + let instructions = authorize_stake_accounts_instructions( + stake_account_address, + stake_authority_pubkey, + withdraw_authority_pubkey, + new_stake_authority_pubkey, + new_withdraw_authority_pubkey, + ); + Message::new_with_payer(&instructions, Some(&fee_payer_pubkey)) + }) + .collect::>() +} + +pub(crate) fn rebase_stake_accounts( + fee_payer_pubkey: &Pubkey, + new_base_pubkey: &Pubkey, + stake_authority_pubkey: &Pubkey, + balances: &[(Pubkey, u64)], +) -> Vec { + balances + .iter() + .enumerate() + .map(|(i, (stake_account_address, lamports))| { + rebase_stake_account( + stake_account_address, + new_base_pubkey, + i, + fee_payer_pubkey, + stake_authority_pubkey, + *lamports, + ) + }) + .collect() +} + +pub(crate) fn move_stake_accounts( + fee_payer_pubkey: &Pubkey, + new_base_pubkey: &Pubkey, + stake_authority_pubkey: &Pubkey, + withdraw_authority_pubkey: &Pubkey, + new_stake_authority_pubkey: &Pubkey, + new_withdraw_authority_pubkey: &Pubkey, + balances: &[(Pubkey, u64)], +) -> Vec { + balances + .iter() + .enumerate() + .map(|(i, (stake_account_address, lamports))| { + move_stake_account( + stake_account_address, + new_base_pubkey, + i, + fee_payer_pubkey, + stake_authority_pubkey, + withdraw_authority_pubkey, + new_stake_authority_pubkey, + new_withdraw_authority_pubkey, + *lamports, + ) + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use solana_runtime::{bank::Bank, bank_client::BankClient}; + use solana_sdk::{ + account::Account, + client::SyncClient, + genesis_config::create_genesis_config, + signature::{Keypair, Signer}, + }; + use solana_stake_program::stake_state::StakeState; + + fn create_bank(lamports: u64) -> (Bank, Keypair, u64) { + let (genesis_config, mint_keypair) = create_genesis_config(lamports); + let mut bank = Bank::new(&genesis_config); + bank.add_instruction_processor( + solana_stake_program::id(), + solana_stake_program::stake_instruction::process_instruction, + ); + let rent = bank.get_minimum_balance_for_rent_exemption(std::mem::size_of::()); + (bank, mint_keypair, rent) + } + + fn create_account( + client: &C, + funding_keypair: &Keypair, + lamports: u64, + ) -> Keypair { + let fee_payer_keypair = Keypair::new(); + client + .transfer(lamports, &funding_keypair, &fee_payer_keypair.pubkey()) + .unwrap(); + fee_payer_keypair + } + + fn get_account_at(client: &C, base_pubkey: &Pubkey, i: usize) -> Account { + let account_address = derive_stake_account_address(&base_pubkey, i); + client.get_account(&account_address).unwrap().unwrap() + } + + fn get_balances( + client: &C, + base_pubkey: &Pubkey, + num_accounts: usize, + ) -> Vec<(Pubkey, u64)> { + (0..num_accounts) + .into_iter() + .map(|i| { + let address = derive_stake_account_address(&base_pubkey, i); + (address, client.get_balance(&address).unwrap()) + }) + .collect() + } + + #[test] + fn test_new_derived_stake_account() { + 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 stake_authority_pubkey = Pubkey::new_rand(); + let withdraw_authority_pubkey = Pubkey::new_rand(); + + let message = new_stake_account( + &fee_payer_pubkey, + &funding_pubkey, + &base_pubkey, + lamports, + &stake_authority_pubkey, + &withdraw_authority_pubkey, + 0, + ); + + let signers = [&funding_keypair, &fee_payer_keypair, &base_keypair]; + bank_client.send_message(&signers, message).unwrap(); + + let account = get_account_at(&bank_client, &base_pubkey, 0); + assert_eq!(account.lamports, lamports); + let authorized = StakeState::authorized_from(&account).unwrap(); + assert_eq!(authorized.staker, stake_authority_pubkey); + assert_eq!(authorized.withdrawer, withdraw_authority_pubkey); + } + + #[test] + fn test_authorize_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 stake_authority_keypair = Keypair::new(); + let stake_authority_pubkey = stake_authority_keypair.pubkey(); + let withdraw_authority_keypair = Keypair::new(); + let withdraw_authority_pubkey = withdraw_authority_keypair.pubkey(); + + let message = new_stake_account( + &fee_payer_pubkey, + &funding_pubkey, + &base_pubkey, + lamports, + &stake_authority_pubkey, + &withdraw_authority_pubkey, + 0, + ); + + let signers = [&funding_keypair, &fee_payer_keypair, &base_keypair]; + bank_client.send_message(&signers, message).unwrap(); + + let new_stake_authority_pubkey = Pubkey::new_rand(); + let new_withdraw_authority_pubkey = Pubkey::new_rand(); + let messages = authorize_stake_accounts( + &fee_payer_pubkey, + &base_pubkey, + &stake_authority_pubkey, + &withdraw_authority_pubkey, + &new_stake_authority_pubkey, + &new_withdraw_authority_pubkey, + 1, + ); + + let signers = [ + &fee_payer_keypair, + &stake_authority_keypair, + &withdraw_authority_keypair, + ]; + for message in messages { + bank_client.send_message(&signers, message).unwrap(); + } + + let account = get_account_at(&bank_client, &base_pubkey, 0); + let authorized = StakeState::authorized_from(&account).unwrap(); + assert_eq!(authorized.staker, new_stake_authority_pubkey); + assert_eq!(authorized.withdrawer, new_withdraw_authority_pubkey); + } + + #[test] + fn test_rebase_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 stake_authority_keypair = Keypair::new(); + let stake_authority_pubkey = stake_authority_keypair.pubkey(); + let withdraw_authority_keypair = Keypair::new(); + let withdraw_authority_pubkey = withdraw_authority_keypair.pubkey(); + + let num_accounts = 1; + let message = new_stake_account( + &fee_payer_pubkey, + &funding_pubkey, + &base_pubkey, + lamports, + &stake_authority_pubkey, + &withdraw_authority_pubkey, + 0, + ); + + let signers = [&funding_keypair, &fee_payer_keypair, &base_keypair]; + bank_client.send_message(&signers, message).unwrap(); + + let new_base_keypair = Keypair::new(); + let new_base_pubkey = new_base_keypair.pubkey(); + let balances = get_balances(&bank_client, &base_pubkey, num_accounts); + let messages = rebase_stake_accounts( + &fee_payer_pubkey, + &new_base_pubkey, + &stake_authority_pubkey, + &balances, + ); + assert_eq!(messages.len(), num_accounts); + + let signers = [ + &fee_payer_keypair, + &new_base_keypair, + &stake_authority_keypair, + ]; + for message in messages { + bank_client.send_message(&signers, message).unwrap(); + } + + // Ensure the new accounts are duplicates of the previous ones. + let account = get_account_at(&bank_client, &new_base_pubkey, 0); + let authorized = StakeState::authorized_from(&account).unwrap(); + assert_eq!(authorized.staker, stake_authority_pubkey); + assert_eq!(authorized.withdrawer, withdraw_authority_pubkey); + } + + #[test] + fn test_move_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 stake_authority_keypair = Keypair::new(); + let stake_authority_pubkey = stake_authority_keypair.pubkey(); + let withdraw_authority_keypair = Keypair::new(); + let withdraw_authority_pubkey = withdraw_authority_keypair.pubkey(); + + let num_accounts = 1; + let message = new_stake_account( + &fee_payer_pubkey, + &funding_pubkey, + &base_pubkey, + lamports, + &stake_authority_pubkey, + &withdraw_authority_pubkey, + 0, + ); + + let signers = [&funding_keypair, &fee_payer_keypair, &base_keypair]; + bank_client.send_message(&signers, message).unwrap(); + + let new_base_keypair = Keypair::new(); + let new_base_pubkey = new_base_keypair.pubkey(); + let new_stake_authority_pubkey = Pubkey::new_rand(); + let new_withdraw_authority_pubkey = Pubkey::new_rand(); + let balances = get_balances(&bank_client, &base_pubkey, num_accounts); + let messages = move_stake_accounts( + &fee_payer_pubkey, + &new_base_pubkey, + &stake_authority_pubkey, + &withdraw_authority_pubkey, + &new_stake_authority_pubkey, + &new_withdraw_authority_pubkey, + &balances, + ); + assert_eq!(messages.len(), num_accounts); + + let signers = [ + &fee_payer_keypair, + &new_base_keypair, + &stake_authority_keypair, + &withdraw_authority_keypair, + ]; + for message in messages { + bank_client.send_message(&signers, message).unwrap(); + } + + // Ensure the new accounts have the new authorities. + let account = get_account_at(&bank_client, &new_base_pubkey, 0); + let authorized = StakeState::authorized_from(&account).unwrap(); + assert_eq!(authorized.staker, new_stake_authority_pubkey); + assert_eq!(authorized.withdrawer, new_withdraw_authority_pubkey); + } +}