solana-program-library/stake-pool/cli/src/main.rs

1375 lines
48 KiB
Rust

#[macro_use]
extern crate lazy_static;
mod client;
use {
crate::client::*,
clap::{
crate_description, crate_name, crate_version, value_t, value_t_or_exit, App, AppSettings,
Arg, ArgGroup, SubCommand,
},
solana_clap_utils::{
input_parsers::pubkey_of,
input_validators::{is_amount, is_keypair, is_parsable, is_pubkey, is_url},
keypair::signer_from_path,
},
solana_client::{rpc_client::RpcClient, rpc_response::StakeActivationState},
solana_program::{
borsh::get_packed_len, instruction::Instruction, program_pack::Pack, pubkey::Pubkey,
},
solana_sdk::{
commitment_config::CommitmentConfig,
native_token::{self, Sol},
signature::{Keypair, Signer},
system_instruction,
transaction::Transaction,
},
spl_stake_pool::{
self,
borsh::get_instance_packed_len,
find_deposit_authority_program_address, find_stake_program_address,
find_withdraw_authority_program_address,
stake_program::{self, StakeAuthorize, StakeState},
state::{StakePool, ValidatorList},
},
std::process::exit,
};
struct Config {
rpc_client: RpcClient,
verbose: bool,
owner: Box<dyn Signer>,
fee_payer: Box<dyn Signer>,
dry_run: bool,
no_update: bool,
}
type Error = Box<dyn std::error::Error>;
type CommandResult = Result<(), Error>;
const STAKE_STATE_LEN: usize = 200;
const MAX_ACCOUNTS_TO_UPDATE: usize = 10;
lazy_static! {
static ref MIN_STAKE_BALANCE: u64 = native_token::sol_to_lamports(1.0);
}
macro_rules! unique_signers {
($vec:ident) => {
$vec.sort_by_key(|l| l.pubkey());
$vec.dedup();
};
}
fn check_fee_payer_balance(config: &Config, required_balance: u64) -> Result<(), Error> {
let balance = config.rpc_client.get_balance(&config.fee_payer.pubkey())?;
if balance < required_balance {
Err(format!(
"Fee payer, {}, has insufficient balance: {} required, {} available",
config.fee_payer.pubkey(),
Sol(required_balance),
Sol(balance)
)
.into())
} else {
Ok(())
}
}
fn send_transaction(
config: &Config,
transaction: Transaction,
) -> solana_client::client_error::Result<()> {
if config.dry_run {
let result = config.rpc_client.simulate_transaction(&transaction)?;
println!("Simulate result: {:?}", result);
} else {
let signature = config
.rpc_client
.send_and_confirm_transaction_with_spinner(&transaction)?;
println!("Signature: {}", signature);
}
Ok(())
}
fn command_create_pool(
config: &Config,
fee: spl_stake_pool::instruction::Fee,
max_validators: u32,
) -> CommandResult {
let mint_account = Keypair::new();
println!("Creating mint {}", mint_account.pubkey());
let pool_fee_account = Keypair::new();
println!(
"Creating pool fee collection account {}",
pool_fee_account.pubkey()
);
let stake_pool_keypair = Keypair::new();
println!("Creating stake pool {}", stake_pool_keypair.pubkey());
let validator_list = Keypair::new();
let mint_account_balance = config
.rpc_client
.get_minimum_balance_for_rent_exemption(spl_token::state::Mint::LEN)?;
let pool_fee_account_balance = config
.rpc_client
.get_minimum_balance_for_rent_exemption(spl_token::state::Account::LEN)?;
let stake_pool_account_lamports = config
.rpc_client
.get_minimum_balance_for_rent_exemption(get_packed_len::<StakePool>())?;
let empty_validator_list = ValidatorList::new(max_validators);
let validator_list_size = get_instance_packed_len(&empty_validator_list)?;
let validator_list_balance = config
.rpc_client
.get_minimum_balance_for_rent_exemption(validator_list_size)?;
let total_rent_free_balances = mint_account_balance
+ pool_fee_account_balance
+ stake_pool_account_lamports
+ validator_list_balance;
let default_decimals = spl_token::native_mint::DECIMALS;
// Calculate withdraw authority used for minting pool tokens
let (withdraw_authority, _) = find_withdraw_authority_program_address(
&spl_stake_pool::id(),
&stake_pool_keypair.pubkey(),
);
if config.verbose {
println!("Stake pool withdraw authority {}", withdraw_authority);
}
let mut transaction = Transaction::new_with_payer(
&[
// Account for the stake pool mint
system_instruction::create_account(
&config.fee_payer.pubkey(),
&mint_account.pubkey(),
mint_account_balance,
spl_token::state::Mint::LEN as u64,
&spl_token::id(),
),
// Account for the pool fee accumulation
system_instruction::create_account(
&config.fee_payer.pubkey(),
&pool_fee_account.pubkey(),
pool_fee_account_balance,
spl_token::state::Account::LEN as u64,
&spl_token::id(),
),
// Account for the stake pool
system_instruction::create_account(
&config.fee_payer.pubkey(),
&stake_pool_keypair.pubkey(),
stake_pool_account_lamports,
get_packed_len::<StakePool>() as u64,
&spl_stake_pool::id(),
),
// Validator stake account list storage
system_instruction::create_account(
&config.fee_payer.pubkey(),
&validator_list.pubkey(),
validator_list_balance,
validator_list_size as u64,
&spl_stake_pool::id(),
),
// Initialize pool token mint account
spl_token::instruction::initialize_mint(
&spl_token::id(),
&mint_account.pubkey(),
&withdraw_authority,
None,
default_decimals,
)?,
// Initialize fee receiver account
spl_token::instruction::initialize_account(
&spl_token::id(),
&pool_fee_account.pubkey(),
&mint_account.pubkey(),
&config.owner.pubkey(),
)?,
// Initialize stake pool account
spl_stake_pool::instruction::initialize(
&spl_stake_pool::id(),
&stake_pool_keypair.pubkey(),
&config.owner.pubkey(),
&validator_list.pubkey(),
&mint_account.pubkey(),
&pool_fee_account.pubkey(),
&spl_token::id(),
fee,
max_validators,
)?,
],
Some(&config.fee_payer.pubkey()),
);
let (recent_blockhash, fee_calculator) = config.rpc_client.get_recent_blockhash()?;
check_fee_payer_balance(
config,
total_rent_free_balances + fee_calculator.calculate_fee(&transaction.message()),
)?;
let mut signers = vec![
config.fee_payer.as_ref(),
&stake_pool_keypair,
&validator_list,
&mint_account,
&pool_fee_account,
config.owner.as_ref(),
];
unique_signers!(signers);
transaction.sign(&signers, recent_blockhash);
send_transaction(&config, transaction)?;
Ok(())
}
fn command_vsa_create(
config: &Config,
stake_pool_address: &Pubkey,
vote_account: &Pubkey,
) -> CommandResult {
let (stake_account, _) =
find_stake_program_address(&spl_stake_pool::id(), &vote_account, &stake_pool_address);
println!("Creating stake account {}", stake_account);
let mut transaction = Transaction::new_with_payer(
&[
// Create new validator stake account address
spl_stake_pool::instruction::create_validator_stake_account(
&spl_stake_pool::id(),
&stake_pool_address,
&config.owner.pubkey(),
&config.fee_payer.pubkey(),
&stake_account,
&vote_account,
)?,
],
Some(&config.fee_payer.pubkey()),
);
let (recent_blockhash, fee_calculator) = config.rpc_client.get_recent_blockhash()?;
check_fee_payer_balance(config, fee_calculator.calculate_fee(&transaction.message()))?;
transaction.sign(
&[config.fee_payer.as_ref(), config.owner.as_ref()],
recent_blockhash,
);
send_transaction(&config, transaction)?;
Ok(())
}
fn command_vsa_add(
config: &Config,
stake_pool_address: &Pubkey,
stake: &Pubkey,
token_receiver: &Option<Pubkey>,
) -> CommandResult {
if config.rpc_client.get_stake_activation(*stake, None)?.state != StakeActivationState::Active {
return Err("Stake account is not active.".into());
}
if !config.no_update {
command_update(config, stake_pool_address)?;
}
let stake_pool = get_stake_pool(&config.rpc_client, stake_pool_address)?;
let mut total_rent_free_balances: u64 = 0;
let token_receiver_account = Keypair::new();
let mut instructions: Vec<Instruction> = vec![];
let mut signers = vec![config.fee_payer.as_ref(), config.owner.as_ref()];
// Create token account if not specified
let token_receiver = unwrap_create_token_account(
&config,
&token_receiver,
&token_receiver_account,
&stake_pool.pool_mint,
&mut instructions,
|balance| {
signers.push(&token_receiver_account);
total_rent_free_balances += balance;
},
)?;
// Calculate Deposit and Withdraw stake pool authorities
let pool_deposit_authority =
find_deposit_authority_program_address(&spl_stake_pool::id(), stake_pool_address).0;
let pool_withdraw_authority =
find_withdraw_authority_program_address(&spl_stake_pool::id(), stake_pool_address).0;
instructions.extend(vec![
// Set Withdrawer on stake account to Deposit authority of the stake pool
stake_program::authorize(
&stake,
&config.owner.pubkey(),
&pool_deposit_authority,
StakeAuthorize::Withdrawer,
),
// Set Staker on stake account to Deposit authority of the stake pool
stake_program::authorize(
&stake,
&config.owner.pubkey(),
&pool_deposit_authority,
StakeAuthorize::Staker,
),
// Add validator stake account to the pool
spl_stake_pool::instruction::add_validator_to_pool(
&spl_stake_pool::id(),
&stake_pool_address,
&config.owner.pubkey(),
&pool_deposit_authority,
&pool_withdraw_authority,
&stake_pool.validator_list,
&stake,
&token_receiver,
&stake_pool.pool_mint,
&spl_token::id(),
)?,
]);
let mut transaction =
Transaction::new_with_payer(&instructions, Some(&config.fee_payer.pubkey()));
let (recent_blockhash, fee_calculator) = config.rpc_client.get_recent_blockhash()?;
check_fee_payer_balance(
config,
total_rent_free_balances + fee_calculator.calculate_fee(&transaction.message()),
)?;
unique_signers!(signers);
transaction.sign(&signers, recent_blockhash);
send_transaction(&config, transaction)?;
Ok(())
}
fn command_vsa_remove(
config: &Config,
stake_pool_address: &Pubkey,
stake: &Pubkey,
withdraw_from: &Pubkey,
new_authority: &Option<Pubkey>,
) -> CommandResult {
if !config.no_update {
command_update(config, stake_pool_address)?;
}
let stake_pool = get_stake_pool(&config.rpc_client, stake_pool_address)?;
let pool_withdraw_authority =
find_withdraw_authority_program_address(&spl_stake_pool::id(), stake_pool_address).0;
let owner_pubkey = config.owner.pubkey();
let new_authority = new_authority.as_ref().unwrap_or(&owner_pubkey);
// Calculate amount of tokens to withdraw
let stake_account = config.rpc_client.get_account(&stake)?;
let tokens_to_withdraw = stake_pool
.calc_pool_withdraw_amount(stake_account.lamports)
.unwrap();
// Check balance and mint
let token_account =
get_token_account(&config.rpc_client, &withdraw_from, &stake_pool.pool_mint)?;
if token_account.amount < tokens_to_withdraw {
let pool_mint = get_token_mint(&config.rpc_client, &stake_pool.pool_mint)?;
return Err(format!(
"Not enough balance to burn to remove validator stake account from the pool. {} pool tokens needed.",
spl_token::amount_to_ui_amount(tokens_to_withdraw, pool_mint.decimals)
).into());
}
let mut transaction = Transaction::new_with_payer(
&[
// Approve spending token
spl_token::instruction::approve(
&spl_token::id(),
&withdraw_from,
&pool_withdraw_authority,
&config.owner.pubkey(),
&[],
tokens_to_withdraw,
)?,
// Create new validator stake account address
spl_stake_pool::instruction::remove_validator_from_pool(
&spl_stake_pool::id(),
&stake_pool_address,
&config.owner.pubkey(),
&pool_withdraw_authority,
&new_authority,
&stake_pool.validator_list,
&stake,
&withdraw_from,
&stake_pool.pool_mint,
&spl_token::id(),
)?,
],
Some(&config.fee_payer.pubkey()),
);
let (recent_blockhash, fee_calculator) = config.rpc_client.get_recent_blockhash()?;
check_fee_payer_balance(config, fee_calculator.calculate_fee(&transaction.message()))?;
transaction.sign(
&[config.fee_payer.as_ref(), config.owner.as_ref()],
recent_blockhash,
);
send_transaction(&config, transaction)?;
Ok(())
}
fn unwrap_create_token_account<F>(
config: &Config,
token_optional: &Option<Pubkey>,
keypair: &Keypair,
mint: &Pubkey,
instructions: &mut Vec<Instruction>,
handler: F,
) -> Result<Pubkey, Error>
where
F: FnOnce(u64),
{
let result = match token_optional {
Some(value) => *value,
None => {
// Account for tokens not specified, creating one
println!("Creating account to receive tokens {}", keypair.pubkey());
let min_account_balance = config
.rpc_client
.get_minimum_balance_for_rent_exemption(spl_token::state::Account::LEN)?;
instructions.extend(vec![
// Creating new account
system_instruction::create_account(
&config.fee_payer.pubkey(),
&keypair.pubkey(),
min_account_balance,
spl_token::state::Account::LEN as u64,
&spl_token::id(),
),
// Initialize token receiver account
spl_token::instruction::initialize_account(
&spl_token::id(),
&keypair.pubkey(),
mint,
&config.owner.pubkey(),
)?,
]);
handler(min_account_balance);
keypair.pubkey()
}
};
Ok(result)
}
fn command_deposit(
config: &Config,
stake_pool_address: &Pubkey,
stake: &Pubkey,
token_receiver: &Option<Pubkey>,
) -> CommandResult {
if !config.no_update {
command_update(config, stake_pool_address)?;
}
let stake_pool = get_stake_pool(&config.rpc_client, stake_pool_address)?;
let stake_state = get_stake_state(&config.rpc_client, &stake)?;
if config.verbose {
println!("Depositing stake account {:?}", stake_state);
}
let vote_account = match stake_state {
StakeState::Stake(_, stake) => Ok(stake.delegation.voter_pubkey),
_ => Err("Wrong stake account state, must be delegated to validator"),
}?;
// Check if this vote account has staking account in the pool
let validator_list = get_validator_list(&config.rpc_client, &stake_pool.validator_list)?;
if !validator_list.contains(&vote_account) {
return Err("Stake account for this validator does not exist in the pool.".into());
}
// Calculate validator stake account address linked to the pool
let (validator_stake_account, _) =
find_stake_program_address(&spl_stake_pool::id(), &vote_account, stake_pool_address);
let validator_stake_state = get_stake_state(&config.rpc_client, &validator_stake_account)?;
println!("Depositing into stake account {}", validator_stake_account);
if config.verbose {
println!("{:?}", validator_stake_state);
}
let mut instructions: Vec<Instruction> = vec![];
let mut signers = vec![config.fee_payer.as_ref(), config.owner.as_ref()];
let mut total_rent_free_balances: u64 = 0;
let token_receiver_account = Keypair::new();
// Create token account if not specified
let token_receiver = unwrap_create_token_account(
&config,
&token_receiver,
&token_receiver_account,
&stake_pool.pool_mint,
&mut instructions,
|balance| {
signers.push(&token_receiver_account);
total_rent_free_balances += balance;
},
)?;
// Calculate Deposit and Withdraw stake pool authorities
let pool_deposit_authority =
find_deposit_authority_program_address(&spl_stake_pool::id(), stake_pool_address).0;
let pool_withdraw_authority =
find_withdraw_authority_program_address(&spl_stake_pool::id(), stake_pool_address).0;
instructions.extend(vec![
// Set Withdrawer on stake account to Deposit authority of the stake pool
stake_program::authorize(
&stake,
&config.owner.pubkey(),
&pool_deposit_authority,
StakeAuthorize::Withdrawer,
),
// Set Staker on stake account to Deposit authority of the stake pool
stake_program::authorize(
&stake,
&config.owner.pubkey(),
&pool_deposit_authority,
StakeAuthorize::Staker,
),
// Add stake account to the pool
spl_stake_pool::instruction::deposit(
&spl_stake_pool::id(),
&stake_pool_address,
&stake_pool.validator_list,
&pool_deposit_authority,
&pool_withdraw_authority,
&stake,
&validator_stake_account,
&token_receiver,
&stake_pool.owner_fee_account,
&stake_pool.pool_mint,
&spl_token::id(),
)?,
]);
let mut transaction =
Transaction::new_with_payer(&instructions, Some(&config.fee_payer.pubkey()));
let (recent_blockhash, fee_calculator) = config.rpc_client.get_recent_blockhash()?;
check_fee_payer_balance(
config,
total_rent_free_balances + fee_calculator.calculate_fee(&transaction.message()),
)?;
unique_signers!(signers);
transaction.sign(&signers, recent_blockhash);
send_transaction(&config, transaction)?;
Ok(())
}
fn command_list(config: &Config, stake_pool_address: &Pubkey) -> CommandResult {
let stake_pool = get_stake_pool(&config.rpc_client, stake_pool_address)?;
if config.verbose {
println!("Current validator list");
let validator_list = get_validator_list(&config.rpc_client, &stake_pool.validator_list)?;
for validator in validator_list.validators {
println!(
"Vote Account: {}\tBalance: {}\tEpoch: {}",
validator.vote_account, validator.balance, validator.last_update_epoch
);
}
}
let pool_withdraw_authority =
find_withdraw_authority_program_address(&spl_stake_pool::id(), stake_pool_address).0;
let accounts =
get_stake_accounts_by_withdraw_authority(&config.rpc_client, &pool_withdraw_authority)?;
if accounts.is_empty() {
return Err("No accounts found.".to_string().into());
}
let mut total_lamports: u64 = 0;
for (pubkey, lamports, stake_state) in accounts {
total_lamports += lamports;
println!(
"Stake Account: {}\tVote Account: {}\t{}",
pubkey,
stake_state.delegation().expect("delegation").voter_pubkey,
Sol(lamports)
);
}
println!("Total Stake: {}", Sol(total_lamports));
Ok(())
}
fn command_update(config: &Config, stake_pool_address: &Pubkey) -> CommandResult {
let stake_pool = get_stake_pool(&config.rpc_client, stake_pool_address)?;
let validator_list = get_validator_list(&config.rpc_client, &stake_pool.validator_list)?;
let epoch_info = config.rpc_client.get_epoch_info()?;
let accounts_to_update: Vec<Pubkey> = validator_list
.validators
.iter()
.filter_map(|item| {
if item.last_update_epoch >= epoch_info.epoch {
None
} else {
let (stake_account, _) = find_stake_program_address(
&spl_stake_pool::id(),
&item.vote_account,
&stake_pool_address,
);
Some(stake_account)
}
})
.collect();
let mut instructions: Vec<Instruction> = vec![];
for accounts_chunk in accounts_to_update.chunks(MAX_ACCOUNTS_TO_UPDATE) {
instructions.push(spl_stake_pool::instruction::update_validator_list_balance(
&spl_stake_pool::id(),
&stake_pool.validator_list,
&accounts_chunk,
)?);
}
if instructions.is_empty() && stake_pool.last_update_epoch == epoch_info.epoch {
println!("Stake pool balances are up to date, no update required.");
Ok(())
} else {
println!("Updating stake pool...");
instructions.push(spl_stake_pool::instruction::update_stake_pool_balance(
&spl_stake_pool::id(),
stake_pool_address,
&stake_pool.validator_list,
)?);
let mut transaction =
Transaction::new_with_payer(&instructions, Some(&config.fee_payer.pubkey()));
let (recent_blockhash, fee_calculator) = config.rpc_client.get_recent_blockhash()?;
check_fee_payer_balance(config, fee_calculator.calculate_fee(&transaction.message()))?;
transaction.sign(&[config.fee_payer.as_ref()], recent_blockhash);
send_transaction(&config, transaction)?;
Ok(())
}
}
#[derive(PartialEq, Debug)]
struct WithdrawAccount {
address: Pubkey,
pool_amount: u64,
}
fn prepare_withdraw_accounts(
rpc_client: &RpcClient,
stake_pool: &StakePool,
pool_withdraw_authority: &Pubkey,
pool_amount: u64,
) -> Result<Vec<WithdrawAccount>, Error> {
let mut accounts =
get_stake_accounts_by_withdraw_authority(rpc_client, &pool_withdraw_authority)?;
if accounts.is_empty() {
return Err("No accounts found.".to_string().into());
}
let min_balance = rpc_client.get_minimum_balance_for_rent_exemption(STAKE_STATE_LEN)? + 1;
let pool_mint = get_token_mint(rpc_client, &stake_pool.pool_mint)?;
// Sort from highest to lowest balance
accounts.sort_by(|a, b| b.1.cmp(&a.1));
// Prepare the list of accounts to withdraw from
let mut withdraw_from: Vec<WithdrawAccount> = vec![];
let mut remaining_amount = pool_amount;
// Go through available accounts and withdraw from largest to smallest
for (address, lamports, _) in accounts {
if lamports <= min_balance {
continue;
}
let available_for_withdrawal = stake_pool
.calc_lamports_withdraw_amount(lamports - *MIN_STAKE_BALANCE)
.unwrap();
let pool_amount = u64::min(available_for_withdrawal, remaining_amount);
// Those accounts will be withdrawn completely with `claim` instruction
withdraw_from.push(WithdrawAccount {
address,
pool_amount,
});
remaining_amount -= pool_amount;
if remaining_amount == 0 {
break;
}
}
// Not enough stake to withdraw the specified amount
if remaining_amount > 0 {
return Err(format!(
"No stake accounts found in this pool with enough balance to withdraw {} pool tokens.",
spl_token::amount_to_ui_amount(pool_amount, pool_mint.decimals)
)
.into());
}
Ok(withdraw_from)
}
fn command_withdraw(
config: &Config,
stake_pool_address: &Pubkey,
pool_amount: f64,
withdraw_from: &Pubkey,
stake_receiver_param: &Option<Pubkey>,
) -> CommandResult {
if !config.no_update {
command_update(config, stake_pool_address)?;
}
let stake_pool = get_stake_pool(&config.rpc_client, stake_pool_address)?;
let pool_mint = get_token_mint(&config.rpc_client, &stake_pool.pool_mint)?;
let pool_amount = spl_token::ui_amount_to_amount(pool_amount, pool_mint.decimals);
let pool_withdraw_authority =
find_withdraw_authority_program_address(&spl_stake_pool::id(), stake_pool_address).0;
// Check withdraw_from account type
let token_account =
get_token_account(&config.rpc_client, &withdraw_from, &stake_pool.pool_mint)?;
// Check withdraw_from balance
if token_account.amount < pool_amount {
return Err(format!(
"Not enough token balance to withdraw {} pool tokens.\nMaximum withdraw amount is {} pool tokens.",
spl_token::amount_to_ui_amount(pool_amount, pool_mint.decimals),
spl_token::amount_to_ui_amount(token_account.amount, pool_mint.decimals)
)
.into());
}
// Get the list of accounts to withdraw from
let withdraw_accounts = prepare_withdraw_accounts(
&config.rpc_client,
&stake_pool,
&pool_withdraw_authority,
pool_amount,
)?;
// Construct transaction to withdraw from withdraw_accounts account list
let mut instructions: Vec<Instruction> = vec![];
let mut signers = vec![config.fee_payer.as_ref(), config.owner.as_ref()];
let stake_receiver_account = Keypair::new(); // Will be added to signers if creating new account
instructions.push(
// Approve spending token
spl_token::instruction::approve(
&spl_token::id(),
&withdraw_from,
&pool_withdraw_authority,
&config.owner.pubkey(),
&[],
pool_amount,
)?,
);
// Use separate mutable variable because withdraw might create a new account
let mut stake_receiver: Option<Pubkey> = *stake_receiver_param;
let mut total_rent_free_balances = 0;
// Go through prepared accounts and withdraw/claim them
for withdraw_account in withdraw_accounts {
// Convert pool tokens amount to lamports
let sol_withdraw_amount = stake_pool
.calc_lamports_withdraw_amount(withdraw_account.pool_amount)
.unwrap();
println!(
"Withdrawing from account {}, amount {}, {} pool tokens",
withdraw_account.address,
Sol(sol_withdraw_amount),
spl_token::amount_to_ui_amount(withdraw_account.pool_amount, pool_mint.decimals),
);
if stake_receiver.is_none() {
// Account for tokens not specified, creating one
println!(
"Creating account to receive stake {}",
stake_receiver_account.pubkey()
);
let stake_receiver_account_balance = config
.rpc_client
.get_minimum_balance_for_rent_exemption(STAKE_STATE_LEN)?;
instructions.push(
// Creating new account
system_instruction::create_account(
&config.fee_payer.pubkey(),
&stake_receiver_account.pubkey(),
stake_receiver_account_balance,
STAKE_STATE_LEN as u64,
&stake_program::id(),
),
);
signers.push(&stake_receiver_account);
total_rent_free_balances += stake_receiver_account_balance;
stake_receiver = Some(stake_receiver_account.pubkey());
}
instructions.push(spl_stake_pool::instruction::withdraw(
&spl_stake_pool::id(),
&stake_pool_address,
&stake_pool.validator_list,
&pool_withdraw_authority,
&withdraw_account.address,
&stake_receiver.unwrap(), // Cannot be none at this point
&config.owner.pubkey(),
&withdraw_from,
&stake_pool.pool_mint,
&spl_token::id(),
withdraw_account.pool_amount,
)?);
}
let mut transaction =
Transaction::new_with_payer(&instructions, Some(&config.fee_payer.pubkey()));
let (recent_blockhash, fee_calculator) = config.rpc_client.get_recent_blockhash()?;
check_fee_payer_balance(
config,
total_rent_free_balances + fee_calculator.calculate_fee(&transaction.message()),
)?;
unique_signers!(signers);
transaction.sign(&signers, recent_blockhash);
send_transaction(&config, transaction)?;
Ok(())
}
fn command_set_owner(
config: &Config,
stake_pool_address: &Pubkey,
new_owner: &Option<Pubkey>,
new_fee_receiver: &Option<Pubkey>,
) -> CommandResult {
let stake_pool = get_stake_pool(&config.rpc_client, stake_pool_address)?;
// If new accounts are missing in the arguments use the old ones
let new_owner = match new_owner {
None => stake_pool.owner,
Some(value) => *value,
};
let new_fee_receiver = match new_fee_receiver {
None => stake_pool.owner_fee_account,
Some(value) => {
// Check for fee receiver being a valid token account and have to same mint as the stake pool
let token_account =
get_token_account(&config.rpc_client, value, &stake_pool.pool_mint)?;
if token_account.mint != stake_pool.pool_mint {
return Err("Fee receiver account belongs to a different mint"
.to_string()
.into());
}
*value
}
};
let mut transaction = Transaction::new_with_payer(
&[spl_stake_pool::instruction::set_owner(
&spl_stake_pool::id(),
&stake_pool_address,
&config.owner.pubkey(),
&new_owner,
&new_fee_receiver,
)?],
Some(&config.fee_payer.pubkey()),
);
let (recent_blockhash, fee_calculator) = config.rpc_client.get_recent_blockhash()?;
check_fee_payer_balance(config, fee_calculator.calculate_fee(&transaction.message()))?;
let mut signers = vec![config.fee_payer.as_ref(), config.owner.as_ref()];
unique_signers!(signers);
transaction.sign(&signers, recent_blockhash);
send_transaction(&config, transaction)?;
Ok(())
}
fn main() {
solana_logger::setup_with_default("solana=info");
let matches = App::new(crate_name!())
.about(crate_description!())
.version(crate_version!())
.setting(AppSettings::SubcommandRequiredElseHelp)
.arg({
let arg = Arg::with_name("config_file")
.short("C")
.long("config")
.value_name("PATH")
.takes_value(true)
.global(true)
.help("Configuration file to use");
if let Some(ref config_file) = *solana_cli_config::CONFIG_FILE {
arg.default_value(&config_file)
} else {
arg
}
})
.arg(
Arg::with_name("verbose")
.long("verbose")
.short("v")
.takes_value(false)
.global(true)
.help("Show additional information"),
)
.arg(
Arg::with_name("dry_run")
.long("dry-run")
.takes_value(false)
.global(true)
.help("Simluate transaction instead of executing"),
)
.arg(
Arg::with_name("no_update")
.long("no-update")
.takes_value(false)
.global(true)
.help("Do not automatically update the stake pool if needed"),
)
.arg(
Arg::with_name("json_rpc_url")
.long("url")
.value_name("URL")
.takes_value(true)
.validator(is_url)
.help("JSON RPC URL for the cluster. Default from the configuration file."),
)
.arg(
Arg::with_name("owner")
.long("owner")
.value_name("KEYPAIR")
.validator(is_keypair)
.takes_value(true)
.help(
"Specify the stake pool or stake account owner. \
This may be a keypair file, the ASK keyword. \
Defaults to the client keypair.",
),
)
.arg(
Arg::with_name("fee_payer")
.long("fee-payer")
.value_name("KEYPAIR")
.validator(is_keypair)
.takes_value(true)
.help(
"Specify the fee-payer account. \
This may be a keypair file, the ASK keyword. \
Defaults to the client keypair.",
),
)
.subcommand(SubCommand::with_name("create-pool")
.about("Create a new stake pool")
.arg(
Arg::with_name("fee_numerator")
.long("fee-numerator")
.short("n")
.validator(is_parsable::<u64>)
.value_name("NUMERATOR")
.takes_value(true)
.required(true)
.help("Fee numerator, fee amount is numerator divided by denominator."),
)
.arg(
Arg::with_name("fee_denominator")
.long("fee-denominator")
.short("d")
.validator(is_parsable::<u64>)
.value_name("DENOMINATOR")
.takes_value(true)
.required(true)
.help("Fee denominator, fee amount is numerator divided by denominator."),
)
.arg(
Arg::with_name("max_validators")
.long("max-validators")
.short("m")
.validator(is_parsable::<u32>)
.value_name("NUMBER")
.takes_value(true)
.required(true)
.help("Max number of validators included in the stake pool"),
)
)
.subcommand(SubCommand::with_name("create-validator-stake")
.about("Create a new stake account to use with the pool. Must be signed by the pool owner.")
.arg(
Arg::with_name("pool")
.index(1)
.validator(is_pubkey)
.value_name("POOL_ADDRESS")
.takes_value(true)
.required(true)
.help("Stake pool address"),
)
.arg(
Arg::with_name("vote_account")
.index(2)
.validator(is_pubkey)
.value_name("VOTE_ACCOUNT_ADDRESS")
.takes_value(true)
.required(true)
.help("The validator vote account that this stake will be delegated to"),
)
)
.subcommand(SubCommand::with_name("add-validator")
.about("Add validator account to the stake pool. Must be signed by the pool owner.")
.arg(
Arg::with_name("pool")
.index(1)
.validator(is_pubkey)
.value_name("POOL_ADDRESS")
.takes_value(true)
.required(true)
.help("Stake pool address"),
)
.arg(
Arg::with_name("stake_account")
.index(2)
.validator(is_pubkey)
.value_name("STAKE_ACCOUNT_ADDRESS")
.takes_value(true)
.required(true)
.help("Stake account to add to the pool"),
)
.arg(
Arg::with_name("token_receiver")
.long("token-receiver")
.validator(is_pubkey)
.value_name("ADDRESS")
.takes_value(true)
.help("Account to receive pool token. Must be initialized account of the stake pool token. \
Defaults to the new pool token account."),
)
)
.subcommand(SubCommand::with_name("remove-validator")
.about("Remove validator account from the stake pool. Must be signed by the pool owner.")
.arg(
Arg::with_name("pool")
.index(1)
.validator(is_pubkey)
.value_name("POOL_ADDRESS")
.takes_value(true)
.required(true)
.help("Stake pool address"),
)
.arg(
Arg::with_name("stake_account")
.index(2)
.validator(is_pubkey)
.value_name("STAKE_ACCOUNT_ADDRESS")
.takes_value(true)
.required(true)
.help("Stake account to remove from the pool"),
)
.arg(
Arg::with_name("withdraw_from")
.long("withdraw-from")
.validator(is_pubkey)
.value_name("ADDRESS")
.takes_value(true)
.required(true)
.help("Token account to withdraw pool token from. \
Must have enough tokens for the full stake address balance."),
)
.arg(
Arg::with_name("new_authority")
.long("new-authority")
.validator(is_pubkey)
.value_name("ADDRESS")
.takes_value(true)
.help("New authority to set as Staker and Withdrawer in the stake account removed from the pool.
Defaults to the wallet owner pubkey."),
)
)
.subcommand(SubCommand::with_name("deposit")
.about("Add stake account to the stake pool")
.arg(
Arg::with_name("pool")
.index(1)
.validator(is_pubkey)
.value_name("POOL_ADDRESS")
.takes_value(true)
.required(true)
.help("Stake pool address"),
)
.arg(
Arg::with_name("stake_account")
.index(2)
.validator(is_pubkey)
.value_name("STAKE_ACCOUNT_ADDRESS")
.takes_value(true)
.required(true)
.help("Stake address to join the pool"),
)
.arg(
Arg::with_name("token_receiver")
.long("token-receiver")
.validator(is_pubkey)
.value_name("ADDRESS")
.takes_value(true)
.help("Account to receive pool token. Must be initialized account of the stake pool token. \
Defaults to the new pool token account."),
)
)
.subcommand(SubCommand::with_name("list")
.about("List stake accounts managed by this pool")
.arg(
Arg::with_name("pool")
.index(1)
.validator(is_pubkey)
.value_name("POOL_ADDRESS")
.takes_value(true)
.required(true)
.help("Stake pool address."),
)
)
.subcommand(SubCommand::with_name("update")
.about("Updates all balances in the pool after validator stake accounts receive rewards.")
.arg(
Arg::with_name("pool")
.index(1)
.validator(is_pubkey)
.value_name("POOL_ADDRESS")
.takes_value(true)
.required(true)
.help("Stake pool address."),
)
)
.subcommand(SubCommand::with_name("withdraw")
.about("Withdraw amount from the stake pool")
.arg(
Arg::with_name("pool")
.index(1)
.validator(is_pubkey)
.value_name("POOL_ADDRESS")
.takes_value(true)
.required(true)
.help("Stake pool address."),
)
.arg(
Arg::with_name("amount")
.long("amount")
.validator(is_amount)
.value_name("AMOUNT")
.takes_value(true)
.required(true)
.help("Amount of pool tokens to withdraw for activated stake."),
)
.arg(
Arg::with_name("withdraw_from")
.long("withdraw-from")
.validator(is_pubkey)
.value_name("ADDRESS")
.takes_value(true)
.required(true)
.help("Account to withdraw tokens from. Must be owned by the client."),
)
.arg(
Arg::with_name("stake_receiver")
.long("stake-receiver")
.validator(is_pubkey)
.value_name("STAKE_ACCOUNT_ADDRESS")
.takes_value(true)
.help("Stake account to receive SOL from the stake pool. Defaults to a new stake account."),
)
)
.subcommand(SubCommand::with_name("set-owner")
.about("Changes owner or fee receiver account for the stake pool.")
.arg(
Arg::with_name("pool")
.index(1)
.validator(is_pubkey)
.value_name("POOL_ADDRESS")
.takes_value(true)
.required(true)
.help("Stake pool address."),
)
.arg(
Arg::with_name("new_owner")
.long("new-owner")
.validator(is_pubkey)
.value_name("ADDRESS")
.takes_value(true)
.help("Public key for the new stake pool owner."),
)
.arg(
Arg::with_name("new_fee_receiver")
.long("new-fee-receiver")
.validator(is_pubkey)
.value_name("ADDRESS")
.takes_value(true)
.help("Public key for the new account to set as the stake pool fee receiver."),
)
.group(ArgGroup::with_name("new_accounts")
.arg("new_owner")
.arg("new_fee_receiver")
.required(true)
.multiple(true)
)
)
.get_matches();
let mut wallet_manager = None;
let config = {
let cli_config = if let Some(config_file) = matches.value_of("config_file") {
solana_cli_config::Config::load(config_file).unwrap_or_default()
} else {
solana_cli_config::Config::default()
};
let json_rpc_url = value_t!(matches, "json_rpc_url", String)
.unwrap_or_else(|_| cli_config.json_rpc_url.clone());
let owner = signer_from_path(
&matches,
&cli_config.keypair_path,
"owner",
&mut wallet_manager,
)
.unwrap_or_else(|e| {
eprintln!("error: {}", e);
exit(1);
});
let fee_payer = signer_from_path(
&matches,
&cli_config.keypair_path,
"fee_payer",
&mut wallet_manager,
)
.unwrap_or_else(|e| {
eprintln!("error: {}", e);
exit(1);
});
let verbose = matches.is_present("verbose");
let dry_run = matches.is_present("dry_run");
let no_update = matches.is_present("no_update");
Config {
rpc_client: RpcClient::new_with_commitment(json_rpc_url, CommitmentConfig::confirmed()),
verbose,
owner,
fee_payer,
dry_run,
no_update,
}
};
let _ = match matches.subcommand() {
("create-pool", Some(arg_matches)) => {
let numerator = value_t_or_exit!(arg_matches, "fee_numerator", u64);
let denominator = value_t_or_exit!(arg_matches, "fee_denominator", u64);
let max_validators = value_t_or_exit!(arg_matches, "max_validators", u32);
command_create_pool(
&config,
spl_stake_pool::instruction::Fee {
denominator,
numerator,
},
max_validators,
)
}
("create-validator-stake", Some(arg_matches)) => {
let stake_pool_address = pubkey_of(arg_matches, "pool").unwrap();
let vote_account_address = pubkey_of(arg_matches, "vote_account_address").unwrap();
command_vsa_create(&config, &stake_pool_address, &vote_account_address)
}
("add-validator", Some(arg_matches)) => {
let stake_pool_address = pubkey_of(arg_matches, "pool").unwrap();
let stake_account = pubkey_of(arg_matches, "stake_account").unwrap();
let token_receiver: Option<Pubkey> = pubkey_of(arg_matches, "token_receiver");
command_vsa_add(
&config,
&stake_pool_address,
&stake_account,
&token_receiver,
)
}
("remove-validator", Some(arg_matches)) => {
let stake_pool_address = pubkey_of(arg_matches, "pool").unwrap();
let stake_account = pubkey_of(arg_matches, "stake_account").unwrap();
let withdraw_from = pubkey_of(arg_matches, "withdraw_from").unwrap();
let new_authority: Option<Pubkey> = pubkey_of(arg_matches, "new_authority");
command_vsa_remove(
&config,
&stake_pool_address,
&stake_account,
&withdraw_from,
&new_authority,
)
}
("deposit", Some(arg_matches)) => {
let stake_pool_address = pubkey_of(arg_matches, "pool").unwrap();
let stake_account = pubkey_of(arg_matches, "stake_account").unwrap();
let token_receiver: Option<Pubkey> = pubkey_of(arg_matches, "token_receiver");
command_deposit(
&config,
&stake_pool_address,
&stake_account,
&token_receiver,
)
}
("list", Some(arg_matches)) => {
let stake_pool_address = pubkey_of(arg_matches, "pool").unwrap();
command_list(&config, &stake_pool_address)
}
("update", Some(arg_matches)) => {
let stake_pool_address = pubkey_of(arg_matches, "pool").unwrap();
command_update(&config, &stake_pool_address)
}
("withdraw", Some(arg_matches)) => {
let stake_pool_address = pubkey_of(arg_matches, "pool").unwrap();
let withdraw_from = pubkey_of(arg_matches, "withdraw_from").unwrap();
let pool_amount = value_t_or_exit!(arg_matches, "amount", f64);
let stake_receiver: Option<Pubkey> = pubkey_of(arg_matches, "stake_receiver");
command_withdraw(
&config,
&stake_pool_address,
pool_amount,
&withdraw_from,
&stake_receiver,
)
}
("set-owner", Some(arg_matches)) => {
let stake_pool_address = pubkey_of(arg_matches, "pool").unwrap();
let new_owner: Option<Pubkey> = pubkey_of(arg_matches, "new_owner");
let new_fee_receiver: Option<Pubkey> = pubkey_of(arg_matches, "new_fee_receiver");
command_set_owner(&config, &stake_pool_address, &new_owner, &new_fee_receiver)
}
_ => unreachable!(),
}
.map_err(|err| {
eprintln!("{}", err);
exit(1);
});
}