Add solana-stake-accounts CLI tool (#9164)

automerge
This commit is contained in:
Greg Fitzgerald 2020-03-30 16:04:46 -06:00 committed by GitHub
parent 62040cef56
commit 8636ef5e24
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 1262 additions and 0 deletions

14
Cargo.lock generated
View File

@ -4585,6 +4585,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.2.0"

View File

@ -52,6 +52,7 @@ members = [
"sdk",
"sdk-c",
"scripts",
"stake-accounts",
"stake-monitor",
"sys-tuner",
"transaction-status",

View File

@ -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)

View File

@ -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 <FUNDING_KEYPAIR> <BASE_KEYPAIR> <AMOUNT> \
--stake-authority <PUBKEY> --withdraw-authority <PUBKEY>
```
### Count accounts
Count the number of derived accounts:
```bash
solana-stake-accounts count <BASE_PUBKEY>
```
### Get stake account balances
Sum the balance of derived stake accounts:
```bash
solana-stake-accounts balance <BASE_PUBKEY> --num-accounts <NUMBER>
```
### Get stake account addresses
List the address of each stake account derived from the given public key:
```bash
solana-stake-accounts addresses <BASE_PUBKEY> --num-accounts <NUMBER>
```
### Set new authorities
Set new authorities on each derived stake account:
```bash
solana-stake-accounts authorize <BASE_PUBKEY> \
--stake-authority <KEYPAIR> --withdraw-authority <KEYPAIR> \
--new-stake-authority <PUBKEY> --new-withdraw-authority <PUBKEY> \
--num-accounts <NUMBER>
```
### Relocate stake accounts
Relocate stake accounts:
```bash
solana-stake-accounts rebase <BASE_PUBKEY> <NEW_BASE_KEYPAIR> \
--stake-authority <KEYPAIR> --num-accounts <NUMBER>
```
To atomically rebase and authorize each stake account, use the 'move'
command:
```bash
solana-stake-accounts move <BASE_PUBKEY> <NEW_BASE_KEYPAIR> \
--stake-authority <KEYPAIR> --withdraw-authority <KEYPAIR> \
--new-stake-authority <PUBKEY> --new-withdraw-authority <PUBKEY> \
--num-accounts <NUMBER>
```

21
stake-accounts/Cargo.toml Normal file
View File

@ -0,0 +1,21 @@
[package]
name = "solana-stake-accounts"
description = "Blockchain, Rebuilt for Scale"
authors = ["Solana Maintainers <maintainers@solana.com>"]
edition = "2018"
version = "1.2.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.2.0" }
solana-cli-config = { path = "../cli-config", version = "1.2.0" }
solana-client = { path = "../client", version = "1.2.0" }
solana-remote-wallet = { path = "../remote-wallet", version = "1.2.0" }
solana-sdk = { path = "../sdk", version = "1.2.0" }
solana-stake-program = { path = "../programs/stake", version = "1.2.0" }
[dev-dependencies]
solana-runtime = { path = "../runtime", version = "1.2.0" }

382
stake-accounts/src/args.rs Normal file
View File

@ -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<MoveCommandConfig>),
}
pub(crate) struct CommandConfig {
pub config_file: String,
pub url: Option<String>,
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<Item = T>,
T: Into<OsString> + 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<I, T>(args: I) -> CommandConfig
where
I: IntoIterator<Item = T>,
T: Into<OsString> + 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,
}
}

306
stake-accounts/src/main.rs Normal file
View File

@ -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<RemoteWalletManager>>,
key_url: &str,
) -> Result<Box<dyn Signer>, Box<dyn Error>> {
let matches = ArgMatches::default();
signer_from_path(&matches, key_url, "stake authority", wallet_manager)
}
fn resolve_withdraw_authority(
wallet_manager: Option<&Arc<RemoteWalletManager>>,
key_url: &str,
) -> Result<Box<dyn Signer>, Box<dyn Error>> {
let matches = ArgMatches::default();
signer_from_path(&matches, key_url, "withdraw authority", wallet_manager)
}
fn resolve_new_stake_authority(
wallet_manager: Option<&Arc<RemoteWalletManager>>,
key_url: &str,
) -> Result<Pubkey, Box<dyn Error>> {
let matches = ArgMatches::default();
pubkey_from_path(&matches, key_url, "new stake authority", wallet_manager)
}
fn resolve_new_withdraw_authority(
wallet_manager: Option<&Arc<RemoteWalletManager>>,
key_url: &str,
) -> Result<Pubkey, Box<dyn Error>> {
let matches = ArgMatches::default();
pubkey_from_path(&matches, key_url, "new withdraw authority", wallet_manager)
}
fn resolve_fee_payer(
wallet_manager: Option<&Arc<RemoteWalletManager>>,
key_url: &str,
) -> Result<Box<dyn Signer>, Box<dyn Error>> {
let matches = ArgMatches::default();
signer_from_path(&matches, key_url, "fee-payer", wallet_manager)
}
fn resolve_base_pubkey(
wallet_manager: Option<&Arc<RemoteWalletManager>>,
key_url: &str,
) -> Result<Pubkey, Box<dyn Error>> {
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<u64, ClientError> {
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<usize, ClientError> {
let mut i = 0;
while get_balance_at(client, base_pubkey, i)? > 0 {
i += 1;
}
Ok(i)
}
fn get_balances(
client: &RpcClient,
addresses: Vec<Pubkey>,
) -> Result<Vec<(Pubkey, u64)>, 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<RemoteWalletManager>>,
new_config: &NewCommandConfig,
) -> Result<Signature, Box<dyn Error>> {
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<RemoteWalletManager>>,
authorize_config: &AuthorizeCommandConfig,
) -> Result<(), Box<dyn Error>> {
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<RemoteWalletManager>>,
rebase_config: &RebaseCommandConfig,
) -> Result<(), Box<dyn Error>> {
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<RemoteWalletManager>>,
move_config: &MoveCommandConfig,
) -> Result<(), Box<dyn Error>> {
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<S: Signers>(
client: &RpcClient,
message: Message,
signers: &S,
) -> Result<Signature, ClientError> {
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<dyn Error>> {
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(())
}

View File

@ -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<Pubkey> {
(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<Instruction> {
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<Message> {
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::<Vec<_>>()
}
pub(crate) fn rebase_stake_accounts(
fee_payer_pubkey: &Pubkey,
new_base_pubkey: &Pubkey,
stake_authority_pubkey: &Pubkey,
balances: &[(Pubkey, u64)],
) -> Vec<Message> {
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<Message> {
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::<StakeState>());
(bank, mint_keypair, rent)
}
fn create_account<C: SyncClient>(
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<C: SyncClient>(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<C: SyncClient>(
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);
}
}