From 4f4618441cc4fda1601ded5955b3b6d029808839 Mon Sep 17 00:00:00 2001 From: Rob Walker Date: Sun, 29 Sep 2019 21:18:15 -0700 Subject: [PATCH] split wallet staking commands (#6168) * split wallet staking commands * elide real home * unit->UNIT for usage * unit->UNIT, don't try to run SUBCOMMANDS: ;) * more fixup * fixups * actually check * shellcheck * preserve #6158 after rebase * fixup * test * too hard * remove test --- book/build-cli-usage.sh | 34 + book/src/api-reference/.cli.md | 177 +++++ book/src/api-reference/cli.md | 388 ++++++--- book/src/running-validator/validator-stake.md | 14 +- cli/src/input_parsers.rs | 9 + cli/src/lib.rs | 1 + cli/src/stake.rs | 742 ++++++++++++++++++ cli/src/vote.rs | 59 +- cli/src/wallet.rs | 722 +++-------------- client/src/rpc_client.rs | 3 +- core/src/staking_utils.rs | 2 +- local_cluster/src/local_cluster.rs | 2 +- multinode-demo/delegate-stake.sh | 4 +- programs/stake_api/src/stake_instruction.rs | 97 ++- .../stake_tests/tests/stake_instruction.rs | 8 +- 15 files changed, 1457 insertions(+), 805 deletions(-) create mode 100755 book/build-cli-usage.sh create mode 100644 book/src/api-reference/.cli.md create mode 100644 cli/src/stake.rs diff --git a/book/build-cli-usage.sh b/book/build-cli-usage.sh new file mode 100755 index 0000000000..c9af638969 --- /dev/null +++ b/book/build-cli-usage.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -e + +cd "$(dirname "$0")" + +usage=$(cargo -q run -p solana-cli -- -C ~/.foo --help | sed 's|'"$HOME"'|~|g') + +out=${1:-src/api-reference/cli.md} + +cat src/api-reference/.cli.md > "$out" + +section() { + declare mark=${2:-"###"} + declare section=$1 + read -r name rest <<<"$section" + + printf '%s %s +' "$mark" "$name" + printf '```text +%s +``` + +' "$section" +} + +section "$usage" >> "$out" + +in_subcommands=0 +while read -r subcommand rest; do + [[ $subcommand == "SUBCOMMANDS:" ]] && in_subcommands=1 && continue + if ((in_subcommands)); then + section "$(cargo -q run -p solana-cli -- help "$subcommand" | sed 's|'"$HOME"'|~|g')" "####" >> "$out" + fi +done <<<"$usage">>"$out" diff --git a/book/src/api-reference/.cli.md b/book/src/api-reference/.cli.md new file mode 100644 index 0000000000..7034c7751f --- /dev/null +++ b/book/src/api-reference/.cli.md @@ -0,0 +1,177 @@ +# solana CLI + +The [solana-cli crate](https://crates.io/crates/solana-cli) provides a command-line interface tool for Solana + +## Examples + +### Get Pubkey + +```bash +// Command +$ solana address + +// Return + +``` + +### Airdrop SOL/Lamports + +```bash +// Command +$ solana airdrop 2 + +// Return +"2.00000000 SOL" + +// Command +$ solana airdrop 123 --lamports + +// Return +"123 lamports" +``` + +### Get Balance + +```bash +// Command +$ solana balance + +// Return +"3.00050001 SOL" +``` + +### Confirm Transaction + +```bash +// Command +$ solana confirm + +// Return +"Confirmed" / "Not found" / "Transaction failed with error " +``` + +### Deploy program + +```bash +// Command +$ solana deploy + +// Return + +``` + +### Unconditional Immediate Transfer + +```bash +// Command +$ solana pay 123 + +// Return + +``` + +### Post-Dated Transfer + +```bash +// Command +$ solana pay 123 \ + --after 2018-12-24T23:59:00 --require-timestamp-from + +// Return +{signature: , processId: } +``` + +_`require-timestamp-from` is optional. If not provided, the transaction will expect a timestamp signed by this wallet's private key_ + +### Authorized Transfer + +A third party must send a signature to unlock the lamports. + +```bash +// Command +$ solana pay 123 \ + --require-signature-from + +// Return +{signature: , processId: } +``` + +### Post-Dated and Authorized Transfer + +```bash +// Command +$ solana pay 123 \ + --after 2018-12-24T23:59 --require-timestamp-from \ + --require-signature-from + +// Return +{signature: , processId: } +``` + +### Multiple Witnesses + +```bash +// Command +$ solana pay 123 \ + --require-signature-from \ + --require-signature-from + +// Return +{signature: , processId: } +``` + +### Cancelable Transfer + +```bash +// Command +$ solana pay 123 \ + --require-signature-from \ + --cancelable + +// Return +{signature: , processId: } +``` + +### Cancel Transfer + +```bash +// Command +$ solana cancel + +// Return + +``` + +### Send Signature + +```bash +// Command +$ solana send-signature + +// Return + +``` + +### Indicate Elapsed Time + +Use the current system time: + +```bash +// Command +$ solana send-timestamp + +// Return + +``` + +Or specify some other arbitrary timestamp: + +```bash +// Command +$ solana send-timestamp --date 2018-12-24T23:59:00 + +// Return + +``` + +## Usage diff --git a/book/src/api-reference/cli.md b/book/src/api-reference/cli.md index f73c8f86d3..d1509c2bae 100644 --- a/book/src/api-reference/cli.md +++ b/book/src/api-reference/cli.md @@ -175,12 +175,13 @@ $ solana send-timestamp --date 2018-12-24T23:59:00 ``` ## Usage - +### solana-cli ```text -solana 0.12.0 +solana-cli 0.20.0 +Blockchain, Rebuilt for Scale USAGE: - solana [FLAGS] [OPTIONS] [SUBCOMMAND] + solana [OPTIONS] FLAGS: -h, --help Prints help information @@ -194,13 +195,13 @@ OPTIONS: SUBCOMMANDS: address Get your public key airdrop Request lamports - authorize-voter Authorize a new vote signing keypair for the given vote account balance Get your balance cancel Cancel a transfer claim-storage-reward Redeem storage reward credits cluster-version Get the version of the cluster entrypoint confirm Confirm transaction by signature create-replicator-storage-account Create a replicator storage account + create-stake-account Create a stake account create-storage-mining-pool-account Create mining pool account create-validator-storage-account Create a validator storage account create-vote-account Create a vote account @@ -222,12 +223,18 @@ SUBCOMMANDS: show-stake-account Show the contents of a stake account show-storage-account Show the contents of a storage account show-vote-account Show the contents of a vote account + stake-authorize-staker Authorize a new stake signing keypair for the given stake account + stake-authorize-withdrawer Authorize a new withdraw signing keypair for the given stake account + uptime Show the uptime of a validator, based on epoch voting history validator-info Publish/get Validator info on Solana + vote-authorize-voter Authorize a new vote signing keypair for the given vote account + vote-authorize-withdrawer Authorize a new withdraw signing keypair for the given vote account withdraw-stake Withdraw the unstaked lamports from the stake account ``` +#### solana-address ```text -solana-address +solana-address Get your public key USAGE: @@ -243,19 +250,20 @@ OPTIONS: -k, --keypair /path/to/id.json ``` +#### solana-airdrop ```text -solana-airdrop -Request a batch of lamports +solana-airdrop +Request lamports USAGE: - solana airdrop [OPTIONS] [unit] + solana airdrop [OPTIONS] [UNIT] FLAGS: -h, --help Prints help information -V, --version Prints version information OPTIONS: - -C, --config Configuration file to use [default: /Users/tyeraeulberg/.config/solana/wallet/config.yml] + -C, --config Configuration file to use [default: ~/.config/solana/wallet/config.yml] --drone-host Drone host to use [default: the --url host] --drone-port Drone port to use [default: 9900] -u, --url JSON RPC URL for the solana cluster @@ -263,33 +271,12 @@ OPTIONS: ARGS: The airdrop amount to request (default unit SOL) - Specify unit to use for request and balance display [possible values: SOL, lamports] + Specify unit to use for request and balance display [possible values: SOL, lamports] ``` +#### solana-balance ```text -solana-authorize-voter -Authorize a new vote signing keypair for the given vote account - -USAGE: - solana authorize-voter [OPTIONS] - -FLAGS: - -h, --help Prints help information - -V, --version Prints version information - -OPTIONS: - -C, --config Configuration file to use [default: ~/.config/solana/wallet/config.yml] - -u, --url JSON RPC URL for the solana cluster - -k, --keypair /path/to/id.json - -ARGS: - Vote account in which to set the authorized voter - Keypair file for the currently authorized vote signer - New vote signer to authorize -``` - -```text -solana-balance +solana-balance Get your balance USAGE: @@ -309,8 +296,9 @@ ARGS: The public key of the balance to check ``` +#### solana-cancel ```text -solana-cancel +solana-cancel Cancel a transfer USAGE: @@ -329,8 +317,9 @@ ARGS: The process id of the transfer to cancel ``` +#### solana-claim-storage-reward ```text -solana-claim-storage-reward +solana-claim-storage-reward Redeem storage reward credits USAGE: @@ -350,8 +339,9 @@ ARGS: Storage account address to redeem credits for ``` +#### solana-cluster-version ```text -solana-cluster-version +solana-cluster-version Get the version of the cluster entrypoint USAGE: @@ -367,8 +357,9 @@ OPTIONS: -k, --keypair /path/to/id.json ``` +#### solana-confirm ```text -solana-confirm +solana-confirm Confirm transaction by signature USAGE: @@ -387,8 +378,9 @@ ARGS: The transaction signature to confirm ``` +#### solana-create-replicator-storage-account ```text -solana-create-replicator-storage-account +solana-create-replicator-storage-account Create a replicator storage account USAGE: @@ -404,34 +396,64 @@ OPTIONS: -k, --keypair /path/to/id.json ARGS: - - + + ``` +#### solana-create-stake-account ```text -solana-create-storage-mining-pool-account -Create mining pool account +solana-create-stake-account +Create a stake account USAGE: - solana create-storage-mining-pool-account [OPTIONS] [unit] + solana create-stake-account [OPTIONS] [UNIT] FLAGS: -h, --help Prints help information -V, --version Prints version information OPTIONS: - -C, --config Configuration file to use [default: /Users/tyeraeulberg/.config/solana/wallet/config.yml] + --authorized-staker Public key of authorized staker (defaults to wallet) + --authorized-withdrawer Public key of the authorized withdrawer (defaults to wallet) + -C, --config Configuration file to use [default: + ~/.config/solana/wallet/config.yml] + --custodian Identity of the custodian (can withdraw before lockup expires) + -u, --url JSON RPC URL for the solana cluster + -k, --keypair /path/to/id.json + --lockup The slot height at which this account will be available for withdrawal + +ARGS: + Address of the stake account to fund (pubkey or keypair) + The amount of send to the vote account (default unit SOL) + Specify unit to use for request [possible values: SOL, lamports] +``` + +#### solana-create-storage-mining-pool-account +```text +solana-create-storage-mining-pool-account +Create mining pool account + +USAGE: + solana create-storage-mining-pool-account [OPTIONS] [UNIT] + +FLAGS: + -h, --help Prints help information + -V, --version Prints version information + +OPTIONS: + -C, --config Configuration file to use [default: ~/.config/solana/wallet/config.yml] -u, --url JSON RPC URL for the solana cluster -k, --keypair /path/to/id.json ARGS: Storage mining pool account address to fund The amount to assign to the storage mining pool account (default unit SOL) - Specify unit to use for request [possible values: SOL, lamports] + Specify unit to use for request [possible values: SOL, lamports] ``` +#### solana-create-validator-storage-account ```text -solana-create-validator-storage-account +solana-create-validator-storage-account Create a validator storage account USAGE: @@ -447,39 +469,45 @@ OPTIONS: -k, --keypair /path/to/id.json ARGS: - - + + ``` +#### solana-create-vote-account ```text -solana-create-vote-account +solana-create-vote-account Create a vote account USAGE: - solana create-vote-account [OPTIONS] + solana create-vote-account [OPTIONS] [UNIT] FLAGS: -h, --help Prints help information -V, --version Prints version information OPTIONS: - --commission The commission taken on reward redemption (0-255), default: 0 - -C, --config Configuration file to use [default: ~/.config/solana/wallet/config.yml] - -u, --url JSON RPC URL for the solana cluster - -k, --keypair /path/to/id.json + --authorized-voter Public key of the authorized voter (defaults to vote account) + --authorized-withdrawer Public key of the authorized withdrawer (defaults to wallet) + --commission The commission taken on reward redemption (0-255), default: 0 + -C, --config Configuration file to use [default: + ~/.config/solana/wallet/config.yml] + -u, --url JSON RPC URL for the solana cluster + -k, --keypair /path/to/id.json ARGS: Vote account address to fund Validator that will vote with this account - The amount of lamports to send to the vote account + The amount of send to the vote account (default unit SOL) + Specify unit to use for request [possible values: SOL, lamports] ``` +#### solana-deactivate-stake ```text -solana-deactivate-stake +solana-deactivate-stake Deactivate the delegated stake from the stake account USAGE: - solana deactivate-stake [OPTIONS] + solana deactivate-stake [OPTIONS] FLAGS: -h, --help Prints help information @@ -491,35 +519,35 @@ OPTIONS: -k, --keypair /path/to/id.json ARGS: - Keypair file for the stake account, for signing the delegate transaction. - The vote account to which the stake is currently delegated + Stake account to be deactivated. + The vote account to which the stake is currently delegated ``` +#### solana-delegate-stake ```text -solana-delegate-stake +solana-delegate-stake Delegate stake to a vote account USAGE: - solana delegate-stake [OPTIONS] [unit] + solana delegate-stake [OPTIONS] FLAGS: -h, --help Prints help information -V, --version Prints version information OPTIONS: - -C, --config Configuration file to use [default: /Users/tyeraeulberg/.config/solana/wallet/config.yml] + -C, --config Configuration file to use [default: ~/.config/solana/wallet/config.yml] -u, --url JSON RPC URL for the solana cluster -k, --keypair /path/to/id.json ARGS: - Keypair file for the new stake account - The vote account to which the stake will be delegated - The amount to delegate (default unit SOL) - Specify unit to use for request [possible values: SOL, lamports] + Stake account to delegate + The vote account to which the stake will be delegated ``` +#### solana-deploy ```text -solana-deploy +solana-deploy Deploy a program USAGE: @@ -538,8 +566,9 @@ ARGS: /path/to/program.o ``` +#### solana-fees ```text -solana-fees +solana-fees Display current cluster fees USAGE: @@ -555,8 +584,9 @@ OPTIONS: -k, --keypair /path/to/id.json ``` +#### solana-get ```text -solana-get +solana-get Get wallet config settings USAGE: @@ -575,8 +605,9 @@ ARGS: Return a specific config setting [possible values: url, keypair] ``` +#### solana-get-slot ```text -solana-get-slot +solana-get-slot Get current slot USAGE: @@ -592,8 +623,9 @@ OPTIONS: -k, --keypair /path/to/id.json ``` +#### solana-get-transaction-count ```text -solana-get-transaction-count +solana-get-transaction-count Get current transaction count USAGE: @@ -609,21 +641,34 @@ OPTIONS: -k, --keypair /path/to/id.json ``` +#### solana-help ```text -solana-pay +solana-help +Prints this message or the help of the given subcommand(s) + +USAGE: + solana help [subcommand]... + +ARGS: + ... The subcommand whose help message to display +``` + +#### solana-pay +```text +solana-pay Send a payment USAGE: - solana pay [FLAGS] [OPTIONS] [--] [unit] + solana pay [FLAGS] [OPTIONS] [--] [UNIT] FLAGS: - --cancelable + --cancelable -h, --help Prints help information -V, --version Prints version information OPTIONS: -C, --config Configuration file to use [default: - /Users/tyeraeulberg/.config/solana/wallet/config.yml] + ~/.config/solana/wallet/config.yml] -u, --url JSON RPC URL for the solana cluster -k, --keypair /path/to/id.json --after A timestamp after which transaction will execute @@ -631,13 +676,14 @@ OPTIONS: --require-signature-from ... Any third party signatures required to unlock the lamports ARGS: - The public key of recipient + The pubkey of recipient The amount to send (default unit SOL) - Specify unit to use for request [possible values: SOL, lamports] + Specify unit to use for request [possible values: SOL, lamports] ``` +#### solana-ping ```text -solana-ping +solana-ping Submit transactions sequentially USAGE: @@ -648,8 +694,7 @@ FLAGS: -V, --version Prints version information OPTIONS: - -C, --config Configuration file to use [default: - ~/.config/solana/wallet/config.yml] + -C, --config Configuration file to use [default: ~/.config/solana/wallet/config.yml] -c, --count Stop after submitting count transactions -i, --interval Wait interval seconds between submitting the next transaction [default: 2] -u, --url JSON RPC URL for the solana cluster @@ -657,12 +702,13 @@ OPTIONS: -t, --timeout Wait up to timeout seconds for transaction confirmation [default: 10] ``` +#### solana-redeem-vote-credits ```text -solana-redeem-vote-credits +solana-redeem-vote-credits Redeem credits in the stake account USAGE: - solana redeem-vote-credits [OPTIONS] + solana redeem-vote-credits [OPTIONS] FLAGS: -h, --help Prints help information @@ -674,12 +720,13 @@ OPTIONS: -k, --keypair /path/to/id.json ARGS: - Staking account address to redeem credits for - The vote account to which the stake was previously delegated. + Address of the stake account in which to redeem credits + The vote account to which the stake is currently delegated. ``` +#### solana-send-signature ```text -solana-send-signature +solana-send-signature Send a signature to authorize a transfer USAGE: @@ -695,12 +742,13 @@ OPTIONS: -k, --keypair /path/to/id.json ARGS: - The public key of recipient + The pubkey of recipient The process id of the transfer to authorize ``` +#### solana-send-timestamp ```text -solana-send-timestamp +solana-send-timestamp Send a timestamp to unlock a transfer USAGE: @@ -717,12 +765,13 @@ OPTIONS: -k, --keypair /path/to/id.json ARGS: - The public key of recipient + The pubkey of recipient The process id of the transfer to unlock ``` +#### solana-set ```text -solana-set +solana-set Set a wallet config setting USAGE: @@ -738,8 +787,9 @@ OPTIONS: -k, --keypair /path/to/id.json ``` +#### solana-show-account ```text -solana-show-account +solana-show-account Show the contents of an account USAGE: @@ -757,19 +807,21 @@ OPTIONS: -o, --output Write the account data to this file ARGS: - Account public key + Account pubkey ``` +#### solana-show-stake-account ```text -solana-show-stake-account +solana-show-stake-account Show the contents of a stake account USAGE: - solana show-stake-account [OPTIONS] + solana show-stake-account [FLAGS] [OPTIONS] FLAGS: - -h, --help Prints help information - -V, --version Prints version information + -h, --help Prints help information + --lamports Display balance in lamports instead of SOL + -V, --version Prints version information OPTIONS: -C, --config Configuration file to use [default: ~/.config/solana/wallet/config.yml] @@ -777,11 +829,12 @@ OPTIONS: -k, --keypair /path/to/id.json ARGS: - Stake account public key + Address of the stake account to display ``` +#### solana-show-storage-account ```text -solana-show-storage-account +solana-show-storage-account Show the contents of a storage account USAGE: @@ -797,15 +850,38 @@ OPTIONS: -k, --keypair /path/to/id.json ARGS: - Storage account public key + Storage account pubkey ``` +#### solana-show-vote-account ```text -solana-show-vote-account +solana-show-vote-account Show the contents of a vote account USAGE: - solana show-vote-account [OPTIONS] + solana show-vote-account [FLAGS] [OPTIONS] + +FLAGS: + -h, --help Prints help information + --lamports Display balance in lamports instead of SOL + -V, --version Prints version information + +OPTIONS: + -C, --config Configuration file to use [default: ~/.config/solana/wallet/config.yml] + -u, --url JSON RPC URL for the solana cluster + -k, --keypair /path/to/id.json + +ARGS: + Vote account pubkey +``` + +#### solana-stake-authorize-staker +```text +solana-stake-authorize-staker +Authorize a new stake signing keypair for the given stake account + +USAGE: + solana stake-authorize-staker [OPTIONS] FLAGS: -h, --help Prints help information @@ -817,11 +893,58 @@ OPTIONS: -k, --keypair /path/to/id.json ARGS: - Vote account public key + Stake account in which to set the authorized staker + New authorized staker ``` +#### solana-stake-authorize-withdrawer ```text -solana-validator-info +solana-stake-authorize-withdrawer +Authorize a new withdraw signing keypair for the given stake account + +USAGE: + solana stake-authorize-withdrawer [OPTIONS] + +FLAGS: + -h, --help Prints help information + -V, --version Prints version information + +OPTIONS: + -C, --config Configuration file to use [default: ~/.config/solana/wallet/config.yml] + -u, --url JSON RPC URL for the solana cluster + -k, --keypair /path/to/id.json + +ARGS: + Stake account in which to set the authorized withdrawer + New authorized withdrawer +``` + +#### solana-uptime +```text +solana-uptime +Show the uptime of a validator, based on epoch voting history + +USAGE: + solana uptime [FLAGS] [OPTIONS] + +FLAGS: + --aggregate Aggregate uptime data across span + -h, --help Prints help information + -V, --version Prints version information + +OPTIONS: + -C, --config Configuration file to use [default: ~/.config/solana/wallet/config.yml] + -u, --url JSON RPC URL for the solana cluster + -k, --keypair /path/to/id.json + --span Number of recent epochs to examine + +ARGS: + Vote account pubkey +``` + +#### solana-validator-info +```text +solana-validator-info Publish/get Validator info on Solana USAGE: @@ -842,26 +965,71 @@ SUBCOMMANDS: publish Publish Validator info on Solana ``` +#### solana-vote-authorize-voter ```text -solana-withdraw-stake -Withdraw the unstaked lamports from the stake account +solana-vote-authorize-voter +Authorize a new vote signing keypair for the given vote account USAGE: - solana withdraw-stake [OPTIONS] [unit] + solana vote-authorize-voter [OPTIONS] FLAGS: -h, --help Prints help information -V, --version Prints version information OPTIONS: - -C, --config Configuration file to use [default: /Users/tyeraeulberg/.config/solana/wallet/config.yml] + -C, --config Configuration file to use [default: ~/.config/solana/wallet/config.yml] -u, --url JSON RPC URL for the solana cluster -k, --keypair /path/to/id.json ARGS: - Keypair file for the stake account, for signing the withdraw transaction. - The account where the lamports should be transfered - The amount to withdraw from the stake account (default unit SOL) - Specify unit to use for request [possible values: SOL, lamports] + Vote account in which to set the authorized voter + New vote signer to authorize +``` + +#### solana-vote-authorize-withdrawer +```text +solana-vote-authorize-withdrawer +Authorize a new withdraw signing keypair for the given vote account + +USAGE: + solana vote-authorize-withdrawer [OPTIONS] + +FLAGS: + -h, --help Prints help information + -V, --version Prints version information + +OPTIONS: + -C, --config Configuration file to use [default: ~/.config/solana/wallet/config.yml] + -u, --url JSON RPC URL for the solana cluster + -k, --keypair /path/to/id.json + +ARGS: + Vote account in which to set the authorized withdrawer + New withdrawer to authorize +``` + +#### solana-withdraw-stake +```text +solana-withdraw-stake +Withdraw the unstaked lamports from the stake account + +USAGE: + solana withdraw-stake [OPTIONS] [UNIT] + +FLAGS: + -h, --help Prints help information + -V, --version Prints version information + +OPTIONS: + -C, --config Configuration file to use [default: ~/.config/solana/wallet/config.yml] + -u, --url JSON RPC URL for the solana cluster + -k, --keypair /path/to/id.json + +ARGS: + Stake account from which to withdraw + The account to which the lamports should be transfered + The amount to withdraw from the stake account (default unit SOL) + Specify unit to use for request [possible values: SOL, lamports] ``` diff --git a/book/src/running-validator/validator-stake.md b/book/src/running-validator/validator-stake.md index 82da1d1d7e..487d605949 100644 --- a/book/src/running-validator/validator-stake.md +++ b/book/src/running-validator/validator-stake.md @@ -10,15 +10,22 @@ First create a stake account keypair with `solana-keygen`: $ solana-keygen new -o ~/validator-config/stake-keypair.json ``` -and use the cli's `delegate-stake` command to stake your validator with 42 lamports: +and use the cli's `create-stake-account` and `delegate-stake` commands to stake your validator with 42 lamports: ```bash -$ solana delegate-stake ~/validator-config/stake-keypair.json ~/validator-vote-keypair.json 42 lamports +$ solana create-stake-account ~/validator-config/stake-keypair.json 42 lamports +$ solana delegate-stake ~/validator-config/stake-keypair.json ~/validator-vote-keypair.json ``` Note that stakes need to warm up, and warmup increments are applied at Epoch boundaries, so it can take an hour or more for the change to fully take effect. -Assuming your node is voting, now you're up and running and generating validator rewards. You'll want to periodically redeem/claim your rewards: +Stakes can be re-delegated to another node at any time with the same command: + +```bash +$ solana delegate-stake ~/validator-config/stake-keypair.json ~/some-other-validator-vote-keypair.json +``` + +Assuming the node is voting, now you're up and running and generating validator rewards. You'll want to periodically redeem/claim your rewards: ```bash $ solana redeem-vote-credits ~/validator-config/stake-keypair.json ~/validator-vote-keypair.json @@ -37,4 +44,3 @@ The stake will cool down, deactivate over time. While cooling down, your stake w Note that a stake account may only be used once, so after deactivation, use the cli's `withdraw-stake` command to recover the previously staked lamports. Be sure and redeem your credits before withdrawing all your lamports. Once the account is fully withdrawn, the account is destroyed. - diff --git a/cli/src/input_parsers.rs b/cli/src/input_parsers.rs index 47cf3e85a7..2e6337bba6 100644 --- a/cli/src/input_parsers.rs +++ b/cli/src/input_parsers.rs @@ -1,3 +1,4 @@ +use crate::sol_to_lamports; use clap::ArgMatches; use solana_sdk::{ pubkey::Pubkey, @@ -43,6 +44,14 @@ pub fn pubkey_of(matches: &ArgMatches<'_>, name: &str) -> Option { value_of(matches, name).or_else(|| keypair_of(matches, name).map(|keypair| keypair.pubkey())) } +pub fn amount_of(matches: &ArgMatches<'_>, name: &str, unit: &str) -> Option { + if matches.value_of(unit) == Some("lamports") { + value_of(matches, name) + } else { + value_of(matches, name).map(sol_to_lamports) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 9db1458a29..86ccbb1f32 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -5,6 +5,7 @@ pub mod config; pub mod display; pub mod input_parsers; pub mod input_validators; +pub mod stake; pub mod validator_info; pub mod vote; pub mod wallet; diff --git a/cli/src/stake.rs b/cli/src/stake.rs new file mode 100644 index 0000000000..34258d812b --- /dev/null +++ b/cli/src/stake.rs @@ -0,0 +1,742 @@ +use crate::{ + input_parsers::*, + input_validators::*, + wallet::{ + build_balance_message, check_account_for_fee, check_unique_pubkeys, + log_instruction_custom_error, ProcessResult, WalletCommand, WalletConfig, WalletError, + }, +}; +use clap::{App, Arg, ArgMatches, SubCommand}; +use solana_client::rpc_client::RpcClient; +use solana_sdk::{ + account_utils::State, pubkey::Pubkey, signature::KeypairUtil, system_instruction::SystemError, + transaction::Transaction, +}; +use solana_stake_api::{ + stake_instruction::{self, StakeError}, + stake_state::{Authorized, Lockup, StakeAuthorize, StakeState}, +}; +use solana_vote_api::vote_state::VoteState; + +pub trait StakeSubCommands { + fn stake_subcommands(self) -> Self; +} + +impl StakeSubCommands for App<'_, '_> { + fn stake_subcommands(self) -> Self { + self.subcommand( + SubCommand::with_name("create-stake-account") + .about("Create a stake account") + .arg( + Arg::with_name("stake_account_pubkey") + .index(1) + .value_name("STAKE ACCOUNT") + .takes_value(true) + .required(true) + .validator(is_pubkey_or_keypair) + .help("Address of the stake account to fund (pubkey or keypair)") + ) + .arg( + Arg::with_name("amount") + .index(2) + .value_name("AMOUNT") + .takes_value(true) + .required(true) + .help("The amount of send to the vote account (default unit SOL)") + ) + .arg( + Arg::with_name("unit") + .index(3) + .value_name("UNIT") + .takes_value(true) + .possible_values(&["SOL", "lamports"]) + .help("Specify unit to use for request") + ) + .arg( + Arg::with_name("custodian") + .long("custodian") + .value_name("PUBKEY") + .takes_value(true) + .validator(is_pubkey_or_keypair) + .help("Identity of the custodian (can withdraw before lockup expires)") + ) + .arg( + Arg::with_name("lockup") + .long("lockup") + .value_name("SLOT") + .takes_value(true) + .help("The slot height at which this account will be available for withdrawal") + ) + .arg( + Arg::with_name("authorized_staker") + .long("authorized-staker") + .value_name("PUBKEY") + .takes_value(true) + .validator(is_pubkey_or_keypair) + .help("Public key of authorized staker (defaults to wallet)") + ) + .arg( + Arg::with_name("authorized_withdrawer") + .long("authorized-withdrawer") + .value_name("PUBKEY") + .takes_value(true) + .validator(is_pubkey_or_keypair) + .help("Public key of the authorized withdrawer (defaults to wallet)") + ) + ) + .subcommand( + SubCommand::with_name("delegate-stake") + .about("Delegate stake to a vote account") + .arg( + Arg::with_name("force") + .long("force") + .takes_value(false) + .hidden(true) // Don't document this argument to discourage its use + .help("Override vote account sanity checks (use carefully!)") + ) + .arg( + Arg::with_name("stake_account_pubkey") + .index(1) + .value_name("STAKE ACCOUNT") + .takes_value(true) + .required(true) + .validator(is_pubkey_or_keypair) + .help("Stake account to delegate") + ) + .arg( + Arg::with_name("vote_account_pubkey") + .index(2) + .value_name("VOTE ACCOUNT") + .takes_value(true) + .required(true) + .validator(is_pubkey_or_keypair) + .help("The vote account to which the stake will be delegated") + ) + ) + .subcommand( + SubCommand::with_name("stake-authorize-staker") + .about("Authorize a new stake signing keypair for the given stake account") + .arg( + Arg::with_name("stake_account_pubkey") + .index(1) + .value_name("STAKE ACCOUNT") + .takes_value(true) + .required(true) + .validator(is_pubkey_or_keypair) + .help("Stake account in which to set the authorized staker") + ) + .arg( + Arg::with_name("authorized_pubkey") + .index(2) + .value_name("AUTHORIZE PUBKEY") + .takes_value(true) + .required(true) + .validator(is_pubkey_or_keypair) + .help("New authorized staker") + ) + ) + .subcommand( + SubCommand::with_name("stake-authorize-withdrawer") + .about("Authorize a new withdraw signing keypair for the given stake account") + .arg( + Arg::with_name("stake_account_pubkey") + .index(1) + .value_name("STAKE ACCOUNT") + .takes_value(true) + .required(true) + .validator(is_pubkey_or_keypair) + .help("Stake account in which to set the authorized withdrawer") + ) + .arg( + Arg::with_name("authorized_pubkey") + .index(2) + .value_name("AUTHORIZE PUBKEY") + .takes_value(true) + .required(true) + .validator(is_pubkey_or_keypair) + .help("New authorized withdrawer") + ) + ) + .subcommand( + SubCommand::with_name("deactivate-stake") + .about("Deactivate the delegated stake from the stake account") + .arg( + Arg::with_name("stake_account_pubkey") + .index(1) + .value_name("STAKE ACCOUNT") + .takes_value(true) + .required(true) + .help("Stake account to be deactivated.") + ) + .arg( + Arg::with_name("vote_account_pubkey") + .index(2) + .value_name("VOTE ACCOUNT") + .takes_value(true) + .required(true) + .validator(is_pubkey_or_keypair) + .help("The vote account to which the stake is currently delegated") + ) + ) + .subcommand( + SubCommand::with_name("withdraw-stake") + .about("Withdraw the unstaked lamports from the stake account") + .arg( + Arg::with_name("stake_account_pubkey") + .index(1) + .value_name("STAKE ACCOUNT") + .takes_value(true) + .required(true) + .validator(is_pubkey_or_keypair) + .help("Stake account from which to withdraw") + ) + .arg( + Arg::with_name("destination_account_pubkey") + .index(2) + .value_name("DESTINATION ACCOUNT") + .takes_value(true) + .required(true) + .validator(is_pubkey_or_keypair) + .help("The account to which the lamports should be transfered") + ) + .arg( + Arg::with_name("amount") + .index(3) + .value_name("AMOUNT") + .takes_value(true) + .required(true) + .help("The amount to withdraw from the stake account (default unit SOL)") + ) + .arg( + Arg::with_name("unit") + .index(4) + .value_name("UNIT") + .takes_value(true) + .possible_values(&["SOL", "lamports"]) + .help("Specify unit to use for request") + ) + ) + .subcommand( + SubCommand::with_name("redeem-vote-credits") + .about("Redeem credits in the stake account") + .arg( + Arg::with_name("stake_account_pubkey") + .index(1) + .value_name("STAKE ACCOUNT") + .takes_value(true) + .required(true) + .validator(is_pubkey_or_keypair) + .help("Address of the stake account in which to redeem credits") + ) + .arg( + Arg::with_name("vote_account_pubkey") + .index(2) + .value_name("VOTE ACCOUNT") + .takes_value(true) + .required(true) + .validator(is_pubkey_or_keypair) + .help("The vote account to which the stake is currently delegated.") + ) + ) + .subcommand( + SubCommand::with_name("show-stake-account") + .about("Show the contents of a stake account") + .arg( + Arg::with_name("stake_account_pubkey") + .index(1) + .value_name("STAKE ACCOUNT") + .takes_value(true) + .required(true) + .validator(is_pubkey_or_keypair) + .help("Address of the stake account to display") + ) + .arg( + Arg::with_name("lamports") + .long("lamports") + .takes_value(false) + .help("Display balance in lamports instead of SOL") + ) + ) + } +} + +pub fn parse_stake_create_account( + pubkey: &Pubkey, + matches: &ArgMatches<'_>, +) -> Result { + let stake_account_pubkey = pubkey_of(matches, "stake_account_pubkey").unwrap(); + let slot = value_of(&matches, "lockup").unwrap_or(0); + let custodian = pubkey_of(matches, "custodian").unwrap_or_default(); + let staker = pubkey_of(matches, "authorized_staker").unwrap_or(*pubkey); // defaults to config + let withdrawer = pubkey_of(matches, "authorized_withdrawer").unwrap_or(*pubkey); // defaults to config + let lamports = amount_of(matches, "amount", "unit").expect("Invalid amount"); + + Ok(WalletCommand::CreateStakeAccount( + stake_account_pubkey, + Authorized { staker, withdrawer }, + Lockup { custodian, slot }, + lamports, + )) +} + +pub fn parse_stake_delegate_stake(matches: &ArgMatches<'_>) -> Result { + let stake_account_pubkey = pubkey_of(matches, "stake_account_pubkey").unwrap(); + let vote_account_pubkey = pubkey_of(matches, "vote_account_pubkey").unwrap(); + let force = matches.is_present("force"); + + Ok(WalletCommand::DelegateStake( + stake_account_pubkey, + vote_account_pubkey, + force, + )) +} + +pub fn parse_stake_authorize( + matches: &ArgMatches<'_>, + stake_authorize: StakeAuthorize, +) -> Result { + let stake_account_pubkey = pubkey_of(matches, "stake_account_pubkey").unwrap(); + let authorized_pubkey = pubkey_of(matches, "authorized_pubkey").unwrap(); + + Ok(WalletCommand::StakeAuthorize( + stake_account_pubkey, + authorized_pubkey, + stake_authorize, + )) +} + +pub fn parse_redeem_vote_credits(matches: &ArgMatches<'_>) -> Result { + let stake_account_pubkey = pubkey_of(matches, "stake_account_pubkey").unwrap(); + let vote_account_pubkey = pubkey_of(matches, "vote_account_pubkey").unwrap(); + Ok(WalletCommand::RedeemVoteCredits( + stake_account_pubkey, + vote_account_pubkey, + )) +} + +pub fn parse_stake_deactivate_stake( + matches: &ArgMatches<'_>, +) -> Result { + let stake_account_pubkey = pubkey_of(matches, "stake_account_pubkey").unwrap(); + let vote_account_pubkey = pubkey_of(matches, "vote_account_pubkey").unwrap(); + Ok(WalletCommand::DeactivateStake( + stake_account_pubkey, + vote_account_pubkey, + )) +} + +pub fn parse_stake_withdraw_stake(matches: &ArgMatches<'_>) -> Result { + let stake_account_pubkey = pubkey_of(matches, "stake_account_pubkey").unwrap(); + let destination_account_pubkey = pubkey_of(matches, "destination_account_pubkey").unwrap(); + let lamports = amount_of(matches, "amount", "unit").expect("Invalid amount"); + + Ok(WalletCommand::WithdrawStake( + stake_account_pubkey, + destination_account_pubkey, + lamports, + )) +} + +pub fn parse_show_stake_account(matches: &ArgMatches<'_>) -> Result { + let stake_account_pubkey = pubkey_of(matches, "stake_account_pubkey").unwrap(); + let use_lamports_unit = matches.is_present("lamports"); + Ok(WalletCommand::ShowStakeAccount { + pubkey: stake_account_pubkey, + use_lamports_unit, + }) +} + +pub fn process_create_stake_account( + rpc_client: &RpcClient, + config: &WalletConfig, + stake_account_pubkey: &Pubkey, + authorized: &Authorized, + lockup: &Lockup, + lamports: u64, +) -> ProcessResult { + check_unique_pubkeys( + (&config.keypair.pubkey(), "wallet keypair".to_string()), + (stake_account_pubkey, "stake_account_pubkey".to_string()), + )?; + + if rpc_client.get_account(&stake_account_pubkey).is_ok() { + return Err(WalletError::BadParameter(format!( + "Unable to create stake account. Stake account already exists: {}", + stake_account_pubkey + )) + .into()); + } + + let ixs = stake_instruction::create_stake_account_with_lockup( + &config.keypair.pubkey(), + stake_account_pubkey, + authorized, + lockup, + lamports, + ); + let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; + let mut tx = Transaction::new_signed_instructions(&[&config.keypair], ixs, recent_blockhash); + check_account_for_fee(rpc_client, config, &fee_calculator, &tx.message)?; + let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair]); + log_instruction_custom_error::(result) +} + +pub fn process_stake_authorize( + rpc_client: &RpcClient, + config: &WalletConfig, + stake_account_pubkey: &Pubkey, + authorized_pubkey: &Pubkey, + stake_authorize: StakeAuthorize, +) -> ProcessResult { + check_unique_pubkeys( + (stake_account_pubkey, "stake_account_pubkey".to_string()), + (authorized_pubkey, "new_authorized_pubkey".to_string()), + )?; + let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; + let ixs = vec![stake_instruction::authorize( + stake_account_pubkey, // stake account to update + &config.keypair.pubkey(), // currently authorized + authorized_pubkey, // new stake signer + stake_authorize, // stake or withdraw + )]; + + let mut tx = Transaction::new_signed_instructions(&[&config.keypair], ixs, recent_blockhash); + check_account_for_fee(rpc_client, config, &fee_calculator, &tx.message)?; + let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair]); + log_instruction_custom_error::(result) +} + +pub fn process_deactivate_stake_account( + rpc_client: &RpcClient, + config: &WalletConfig, + stake_account_pubkey: &Pubkey, + vote_account_pubkey: &Pubkey, +) -> ProcessResult { + let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; + let ixs = vec![stake_instruction::deactivate_stake( + stake_account_pubkey, + &config.keypair.pubkey(), + vote_account_pubkey, + )]; + let mut tx = Transaction::new_signed_instructions(&[&config.keypair], ixs, recent_blockhash); + check_account_for_fee(rpc_client, config, &fee_calculator, &tx.message)?; + let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair]); + log_instruction_custom_error::(result) +} + +pub fn process_withdraw_stake( + rpc_client: &RpcClient, + config: &WalletConfig, + stake_account_pubkey: &Pubkey, + destination_account_pubkey: &Pubkey, + lamports: u64, +) -> ProcessResult { + let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; + + let ixs = vec![stake_instruction::withdraw( + stake_account_pubkey, + &config.keypair.pubkey(), + destination_account_pubkey, + lamports, + )]; + + let mut tx = Transaction::new_signed_instructions(&[&config.keypair], ixs, recent_blockhash); + check_account_for_fee(rpc_client, config, &fee_calculator, &tx.message)?; + let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair]); + log_instruction_custom_error::(result) +} + +pub fn process_redeem_vote_credits( + rpc_client: &RpcClient, + config: &WalletConfig, + stake_account_pubkey: &Pubkey, + vote_account_pubkey: &Pubkey, +) -> ProcessResult { + let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; + let ixs = vec![stake_instruction::redeem_vote_credits( + stake_account_pubkey, + vote_account_pubkey, + )]; + let mut tx = Transaction::new_signed_with_payer( + ixs, + Some(&config.keypair.pubkey()), + &[&config.keypair], + recent_blockhash, + ); + check_account_for_fee(rpc_client, config, &fee_calculator, &tx.message)?; + let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair]); + log_instruction_custom_error::(result) +} + +pub fn process_show_stake_account( + rpc_client: &RpcClient, + _config: &WalletConfig, + stake_account_pubkey: &Pubkey, + use_lamports_unit: bool, +) -> ProcessResult { + let stake_account = rpc_client.get_account(stake_account_pubkey)?; + if stake_account.owner != solana_stake_api::id() { + Err(WalletError::RpcRequestError( + format!("{:?} is not a stake account", stake_account_pubkey).to_string(), + ))?; + } + fn show_authorized(authorized: &Authorized) { + println!("authorized staker: {}", authorized.staker); + println!("authorized withdrawer: {}", authorized.staker); + } + fn show_lockup(lockup: &Lockup) { + println!("lockup slot: {}", lockup.slot); + println!("lockup custodian: {}", lockup.custodian); + } + match stake_account.state() { + Ok(StakeState::Stake(authorized, lockup, stake)) => { + println!( + "total stake: {}", + build_balance_message(stake_account.lamports, use_lamports_unit) + ); + println!("credits observed: {}", stake.credits_observed); + println!( + "delegated stake: {}", + build_balance_message(stake.stake, use_lamports_unit) + ); + if stake.voter_pubkey != Pubkey::default() { + println!("delegated voter pubkey: {}", stake.voter_pubkey); + } + println!( + "stake activates starting from epoch: {}", + stake.activation_epoch + ); + if stake.deactivation_epoch < std::u64::MAX { + println!( + "stake deactivates starting from epoch: {}", + stake.deactivation_epoch + ); + } + show_authorized(&authorized); + show_lockup(&lockup); + Ok("".to_string()) + } + Ok(StakeState::RewardsPool) => Ok("Stake account is a rewards pool".to_string()), + Ok(StakeState::Uninitialized) => Ok("Stake account is uninitialized".to_string()), + Ok(StakeState::Initialized(authorized, lockup)) => { + println!("Stake account is undelegated"); + show_authorized(&authorized); + show_lockup(&lockup); + Ok("".to_string()) + } + Err(err) => Err(WalletError::RpcRequestError(format!( + "Account data could not be deserialized to stake state: {:?}", + err + )))?, + } +} + +pub fn process_delegate_stake( + rpc_client: &RpcClient, + config: &WalletConfig, + stake_account_pubkey: &Pubkey, + vote_account_pubkey: &Pubkey, + force: bool, +) -> ProcessResult { + check_unique_pubkeys( + (&config.keypair.pubkey(), "wallet keypair".to_string()), + (stake_account_pubkey, "stake_account_pubkey".to_string()), + )?; + + // Sanity check the vote account to ensure it is attached to a validator that has recently + // voted at the tip of the ledger + let vote_account_data = rpc_client + .get_account_data(vote_account_pubkey) + .map_err(|_| { + WalletError::RpcRequestError(format!("Vote account not found: {}", vote_account_pubkey)) + })?; + + let vote_state = VoteState::deserialize(&vote_account_data).map_err(|_| { + WalletError::RpcRequestError( + "Account data could not be deserialized to vote state".to_string(), + ) + })?; + + let sanity_check_result = match vote_state.root_slot { + None => Err(WalletError::BadParameter( + "Unable to delegate. Vote account has no root slot".to_string(), + )), + Some(root_slot) => { + let slot = rpc_client.get_slot()?; + if root_slot + solana_sdk::clock::DEFAULT_SLOTS_PER_TURN < slot { + Err(WalletError::BadParameter( + format!( + "Unable to delegate. Vote account root slot ({}) is too old, the current slot is {}", root_slot, slot + ) + )) + } else { + Ok(()) + } + } + }; + + if sanity_check_result.is_err() { + if !force { + sanity_check_result?; + } else { + println!("--force supplied, ignoring: {:?}", sanity_check_result); + } + } + + let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; + + let ixs = vec![stake_instruction::delegate_stake( + stake_account_pubkey, + &config.keypair.pubkey(), + vote_account_pubkey, + )]; + + let mut tx = Transaction::new_signed_instructions(&[&config.keypair], ixs, recent_blockhash); + check_account_for_fee(rpc_client, config, &fee_calculator, &tx.message)?; + let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair]); + log_instruction_custom_error::(result) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::wallet::{app, parse_command}; + + #[test] + fn test_parse_command() { + let test_commands = app("test", "desc", "version"); + let pubkey = Pubkey::new_rand(); + let pubkey_string = format!("{}", pubkey); + + // // Test AuthorizeStaker Subcommand + // let out_dir = std::env::var("FARF_DIR").unwrap_or_else(|_| "farf".to_string()); + // let keypair = Keypair::new(); + // let keypair_file = format!("{}/tmp/keypair_file-{}", out_dir, keypair.pubkey()); + // let _ = write_keypair(&keypair, &keypair_file).unwrap(); + // + // let test_authorize_staker = test_commands.clone().get_matches_from(vec![ + // "test", + // "stake-authorize-staker", + // &pubkey_string, + // &keypair_file, + // &pubkey_string, + // ]); + // assert_eq!( + // parse_command(&pubkey, &test_authorize_staker).unwrap(), + // WalletCommand::StakeAuthorize(pubkey, keypair, pubkey, StakeAuthorize::Staker) + // ); + // fs::remove_file(&keypair_file).unwrap(); + + // Test CreateVoteAccount SubCommand + let custodian = Pubkey::new_rand(); + let custodian_string = format!("{}", custodian); + let authorized = Pubkey::new_rand(); + let authorized_string = format!("{}", authorized); + let test_create_stake_account = test_commands.clone().get_matches_from(vec![ + "test", + "create-stake-account", + &pubkey_string, + "50", + "--authorized-staker", + &authorized_string, + "--authorized-withdrawer", + &authorized_string, + "--custodian", + &custodian_string, + "--lockup", + "43", + "lamports", + ]); + assert_eq!( + parse_command(&pubkey, &test_create_stake_account).unwrap(), + WalletCommand::CreateStakeAccount( + pubkey, + Authorized { + staker: authorized, + withdrawer: authorized, + }, + Lockup { + slot: 43, + custodian, + }, + 50 + ) + ); + let test_create_stake_account2 = test_commands.clone().get_matches_from(vec![ + "test", + "create-stake-account", + &pubkey_string, + "50", + "lamports", + ]); + assert_eq!( + parse_command(&pubkey, &test_create_stake_account2).unwrap(), + WalletCommand::CreateStakeAccount( + pubkey, + Authorized { + staker: pubkey, + withdrawer: pubkey, + }, + Lockup { + slot: 0, + custodian: Pubkey::default(), + }, + 50 + ) + ); + + // Test DelegateStake Subcommand + let stake_pubkey = Pubkey::new_rand(); + let stake_pubkey_string = stake_pubkey.to_string(); + let test_delegate_stake = test_commands.clone().get_matches_from(vec![ + "test", + "delegate-stake", + &stake_pubkey_string, + &pubkey_string, + ]); + assert_eq!( + parse_command(&pubkey, &test_delegate_stake).unwrap(), + WalletCommand::DelegateStake(stake_pubkey, pubkey, false,) + ); + + let test_delegate_stake = test_commands.clone().get_matches_from(vec![ + "test", + "delegate-stake", + "--force", + &stake_pubkey_string, + &pubkey_string, + ]); + assert_eq!( + parse_command(&pubkey, &test_delegate_stake).unwrap(), + WalletCommand::DelegateStake(stake_pubkey, pubkey, true) + ); + + // Test WithdrawStake Subcommand + let test_withdraw_stake = test_commands.clone().get_matches_from(vec![ + "test", + "withdraw-stake", + &stake_pubkey_string, + &pubkey_string, + "42", + "lamports", + ]); + + assert_eq!( + parse_command(&pubkey, &test_withdraw_stake).unwrap(), + WalletCommand::WithdrawStake(stake_pubkey, pubkey, 42) + ); + + // Test DeactivateStake Subcommand + let test_deactivate_stake = test_commands.clone().get_matches_from(vec![ + "test", + "deactivate-stake", + &stake_pubkey_string, + &pubkey_string, + ]); + assert_eq!( + parse_command(&pubkey, &test_deactivate_stake).unwrap(), + WalletCommand::DeactivateStake(stake_pubkey, pubkey) + ); + } + // TODO: Add process tests +} diff --git a/cli/src/vote.rs b/cli/src/vote.rs index 68c12d64ce..2640cdd189 100644 --- a/cli/src/vote.rs +++ b/cli/src/vote.rs @@ -1,16 +1,14 @@ use crate::{ input_parsers::*, wallet::{ - check_account_for_fee, check_unique_pubkeys, log_instruction_custom_error, ProcessResult, - WalletCommand, WalletConfig, WalletError, + build_balance_message, check_account_for_fee, check_unique_pubkeys, + log_instruction_custom_error, ProcessResult, WalletCommand, WalletConfig, WalletError, }, }; use clap::{value_t_or_exit, ArgMatches}; use solana_client::rpc_client::RpcClient; use solana_sdk::{ - pubkey::Pubkey, - signature::{Keypair, KeypairUtil}, - system_instruction::SystemError, + pubkey::Pubkey, signature::KeypairUtil, system_instruction::SystemError, transaction::Transaction, }; use solana_vote_api::{ @@ -18,19 +16,17 @@ use solana_vote_api::{ vote_state::{VoteAuthorize, VoteInit, VoteState}, }; -pub fn parse_vote_create_account(matches: &ArgMatches<'_>) -> Result { +pub fn parse_vote_create_account( + pubkey: &Pubkey, + matches: &ArgMatches<'_>, +) -> Result { let vote_account_pubkey = pubkey_of(matches, "vote_account_pubkey").unwrap(); let node_pubkey = pubkey_of(matches, "node_pubkey").unwrap(); let commission = value_of(&matches, "commission").unwrap_or(0); let authorized_voter = pubkey_of(matches, "authorized_voter").unwrap_or(vote_account_pubkey); - let authorized_withdrawer = - pubkey_of(matches, "authorized_withdrawer").unwrap_or(vote_account_pubkey); + let authorized_withdrawer = pubkey_of(matches, "authorized_withdrawer").unwrap_or(*pubkey); - let lamports = crate::wallet::parse_amount_lamports( - matches.value_of("amount").unwrap(), - matches.value_of("unit"), - ) - .map_err(|err| WalletError::BadParameter(format!("Invalid amount: {:?}", err)))?; + let lamports = amount_of(matches, "amount", "unit").expect("Invalid amount"); Ok(WalletCommand::CreateVoteAccount( vote_account_pubkey, @@ -49,12 +45,10 @@ pub fn parse_vote_authorize( vote_authorize: VoteAuthorize, ) -> Result { let vote_account_pubkey = pubkey_of(matches, "vote_account_pubkey").unwrap(); - let authorized_keypair = keypair_of(matches, "authorized_keypair_file").unwrap(); let new_authorized_pubkey = pubkey_of(matches, "new_authorized_pubkey").unwrap(); Ok(WalletCommand::VoteAuthorize( vote_account_pubkey, - authorized_keypair, new_authorized_pubkey, vote_authorize, )) @@ -103,7 +97,6 @@ pub fn process_vote_authorize( rpc_client: &RpcClient, config: &WalletConfig, vote_account_pubkey: &Pubkey, - authorized_keypair: &Keypair, new_authorized_pubkey: &Pubkey, vote_authorize: VoteAuthorize, ) -> ProcessResult { @@ -113,21 +106,15 @@ pub fn process_vote_authorize( )?; let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; let ixs = vec![vote_instruction::authorize( - vote_account_pubkey, // vote account to update - &authorized_keypair.pubkey(), // current authorized voter (often the vote account itself) - new_authorized_pubkey, // new vote signer - vote_authorize, // vote or withdraw + vote_account_pubkey, // vote account to update + &config.keypair.pubkey(), // current authorized voter + new_authorized_pubkey, // new vote signer/withdrawer + vote_authorize, // vote or withdraw )]; - let mut tx = Transaction::new_signed_with_payer( - ixs, - Some(&config.keypair.pubkey()), - &[&config.keypair, &authorized_keypair], - recent_blockhash, - ); + let mut tx = Transaction::new_signed_instructions(&[&config.keypair], ixs, recent_blockhash); check_account_for_fee(rpc_client, config, &fee_calculator, &tx.message)?; - let result = - rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair, &authorized_keypair]); + let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair]); log_instruction_custom_error::(result) } @@ -168,7 +155,7 @@ pub fn process_show_vote_account( println!( "account balance: {}", - crate::wallet::build_balance_message(vote_account.lamports, use_lamports_unit) + build_balance_message(vote_account.lamports, use_lamports_unit) ); println!("node id: {}", vote_state.node_pubkey); println!("authorized voter: {}", vote_state.authorized_voter); @@ -290,33 +277,23 @@ pub fn process_uptime( mod tests { use super::*; use crate::wallet::{app, parse_command}; - use solana_sdk::signature::write_keypair; - use std::fs; #[test] fn test_parse_command() { let test_commands = app("test", "desc", "version"); let pubkey = Pubkey::new_rand(); - let pubkey_string = format!("{}", pubkey); - - // Test AuthorizeVoter Subcommand - let out_dir = std::env::var("FARF_DIR").unwrap_or_else(|_| "farf".to_string()); - let keypair = Keypair::new(); - let keypair_file = format!("{}/tmp/keypair_file-{}", out_dir, keypair.pubkey()); - let _ = write_keypair(&keypair, &keypair_file).unwrap(); + let pubkey_string = pubkey.to_string(); let test_authorize_voter = test_commands.clone().get_matches_from(vec![ "test", "vote-authorize-voter", &pubkey_string, - &keypair_file, &pubkey_string, ]); assert_eq!( parse_command(&pubkey, &test_authorize_voter).unwrap(), - WalletCommand::VoteAuthorize(pubkey, keypair, pubkey, VoteAuthorize::Voter) + WalletCommand::VoteAuthorize(pubkey, pubkey, VoteAuthorize::Voter) ); - fs::remove_file(&keypair_file).unwrap(); // Test CreateVoteAccount SubCommand let node_pubkey = Pubkey::new_rand(); diff --git a/cli/src/wallet.rs b/cli/src/wallet.rs index 9727da1c62..dc7fa19067 100644 --- a/cli/src/wallet.rs +++ b/cli/src/wallet.rs @@ -1,6 +1,6 @@ use crate::{ - display::println_name_value, input_parsers::*, input_validators::*, lamports_to_sol, - sol_to_lamports, validator_info::*, vote::*, + display::println_name_value, input_parsers::*, input_validators::*, lamports_to_sol, stake::*, + validator_info::*, vote::*, }; use chrono::prelude::*; use clap::{value_t_or_exit, App, AppSettings, Arg, ArgMatches, SubCommand}; @@ -29,12 +29,9 @@ use solana_sdk::{ system_transaction, transaction::{Transaction, TransactionError}, }; -use solana_stake_api::{ - stake_instruction::{self, StakeError}, - stake_state::{Authorized, Lockup}, -}; +use solana_stake_api::stake_state::{Authorized, Lockup, StakeAuthorize}; use solana_storage_api::storage_instruction; -use solana_vote_api::vote_state::{VoteAuthorize, VoteInit, VoteState}; +use solana_vote_api::vote_state::{VoteAuthorize, VoteInit}; use std::{ collections::VecDeque, fs::File, @@ -67,7 +64,7 @@ pub enum WalletCommand { }, Cancel(Pubkey), Confirm(Signature), - VoteAuthorize(Pubkey, Keypair, Pubkey, VoteAuthorize), + VoteAuthorize(Pubkey, Pubkey, VoteAuthorize), CreateVoteAccount(Pubkey, VoteInit, u64), ShowAccount { pubkey: Pubkey, @@ -83,9 +80,11 @@ pub enum WalletCommand { aggregate: bool, span: Option, }, - DelegateStake(Keypair, Pubkey, u64, Authorized, bool), - WithdrawStake(Keypair, Pubkey, u64), - DeactivateStake(Keypair, Pubkey), + CreateStakeAccount(Pubkey, Authorized, Lockup, u64), + StakeAuthorize(Pubkey, Pubkey, StakeAuthorize), + DelegateStake(Pubkey, Pubkey, bool), + WithdrawStake(Pubkey, Pubkey, u64), + DeactivateStake(Pubkey, Pubkey), RedeemVoteCredits(Pubkey, Pubkey), ShowStakeAccount { pubkey: Pubkey, @@ -202,10 +201,7 @@ pub fn parse_command( } else { None }; - let lamports = parse_amount_lamports( - airdrop_matches.value_of("amount").unwrap(), - airdrop_matches.value_of("unit"), - )?; + let lamports = amount_of(airdrop_matches, "amount", "unit").expect("Invalid amount"); let use_lamports_unit = airdrop_matches.value_of("unit").is_some() && airdrop_matches.value_of("unit").unwrap() == "lamports"; Ok(WalletCommand::Airdrop { @@ -246,7 +242,7 @@ pub fn parse_command( use_lamports_unit, }) } - ("create-vote-account", Some(matches)) => parse_vote_create_account(matches), + ("create-vote-account", Some(matches)) => parse_vote_create_account(pubkey, matches), ("vote-authorize-voter", Some(matches)) => { parse_vote_authorize(matches, VoteAuthorize::Voter) } @@ -255,61 +251,18 @@ pub fn parse_command( } ("show-vote-account", Some(matches)) => parse_vote_get_account_command(matches), ("uptime", Some(matches)) => parse_vote_uptime_command(matches), - ("delegate-stake", Some(matches)) => { - let stake_account_keypair = keypair_of(matches, "stake_account_keypair_file").unwrap(); - let vote_account_pubkey = pubkey_of(matches, "vote_account_pubkey").unwrap(); - let lamports = parse_amount_lamports( - matches.value_of("amount").unwrap(), - matches.value_of("unit"), - )?; - let authorized = Authorized::auto(&stake_account_keypair.pubkey()); - let force = matches.is_present("force"); - Ok(WalletCommand::DelegateStake( - stake_account_keypair, - vote_account_pubkey, - lamports, - authorized, - force, - )) + ("create-stake-account", Some(matches)) => parse_stake_create_account(pubkey, matches), + ("delegate-stake", Some(matches)) => parse_stake_delegate_stake(matches), + ("withdraw-stake", Some(matches)) => parse_stake_withdraw_stake(matches), + ("deactivate-stake", Some(matches)) => parse_stake_deactivate_stake(matches), + ("stake-authorize-staker", Some(matches)) => { + parse_stake_authorize(matches, StakeAuthorize::Staker) } - ("withdraw-stake", Some(matches)) => { - let stake_account_keypair = keypair_of(matches, "stake_account_keypair_file").unwrap(); - let destination_account_pubkey = - pubkey_of(matches, "destination_account_pubkey").unwrap(); - let lamports = parse_amount_lamports( - matches.value_of("amount").unwrap(), - matches.value_of("unit"), - )?; - Ok(WalletCommand::WithdrawStake( - stake_account_keypair, - destination_account_pubkey, - lamports, - )) - } - ("deactivate-stake", Some(matches)) => { - let stake_account_keypair = keypair_of(matches, "stake_account_keypair_file").unwrap(); - let vote_account_pubkey = pubkey_of(matches, "vote_account_pubkey").unwrap(); - Ok(WalletCommand::DeactivateStake( - stake_account_keypair, - vote_account_pubkey, - )) - } - ("redeem-vote-credits", Some(matches)) => { - let stake_account_pubkey = pubkey_of(matches, "stake_account_pubkey").unwrap(); - let vote_account_pubkey = pubkey_of(matches, "vote_account_pubkey").unwrap(); - Ok(WalletCommand::RedeemVoteCredits( - stake_account_pubkey, - vote_account_pubkey, - )) - } - ("show-stake-account", Some(matches)) => { - let stake_account_pubkey = pubkey_of(matches, "stake_account_pubkey").unwrap(); - let use_lamports_unit = matches.is_present("lamports"); - Ok(WalletCommand::ShowStakeAccount { - pubkey: stake_account_pubkey, - use_lamports_unit, - }) + ("stake-authorize-withdrawer", Some(matches)) => { + parse_stake_authorize(matches, StakeAuthorize::Withdrawer) } + ("redeem-vote-credits", Some(matches)) => parse_redeem_vote_credits(matches), + ("show-stake-account", Some(matches)) => parse_show_stake_account(matches), ("create-replicator-storage-account", Some(matches)) => { let account_owner = pubkey_of(matches, "storage_account_owner").unwrap(); let storage_account_pubkey = pubkey_of(matches, "storage_account_pubkey").unwrap(); @@ -349,10 +302,7 @@ pub fn parse_command( ("get-epoch-info", Some(_matches)) => Ok(WalletCommand::GetEpochInfo), ("get-transaction-count", Some(_matches)) => Ok(WalletCommand::GetTransactionCount), ("pay", Some(pay_matches)) => { - let lamports = parse_amount_lamports( - pay_matches.value_of("amount").unwrap(), - pay_matches.value_of("unit"), - )?; + let lamports = amount_of(pay_matches, "amount", "unit").expect("Invalid amount"); let to = value_of(&pay_matches, "to").unwrap_or(*pubkey); let timestamp = if pay_matches.is_present("timestamp") { // Parse input for serde_json @@ -589,231 +539,6 @@ fn process_show_account( Ok("".to_string()) } -fn process_deactivate_stake_account( - rpc_client: &RpcClient, - config: &WalletConfig, - stake_account_keypair: &Keypair, - vote_account_pubkey: &Pubkey, -) -> ProcessResult { - let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; - let ixs = - stake_instruction::deactivate_stake(&stake_account_keypair.pubkey(), vote_account_pubkey); - let mut tx = Transaction::new_signed_with_payer( - vec![ixs], - Some(&config.keypair.pubkey()), - &[&config.keypair, &stake_account_keypair], - recent_blockhash, - ); - check_account_for_fee(rpc_client, config, &fee_calculator, &tx.message)?; - let result = rpc_client - .send_and_confirm_transaction(&mut tx, &[&config.keypair, &stake_account_keypair]); - log_instruction_custom_error::(result) -} - -fn process_delegate_stake( - rpc_client: &RpcClient, - config: &WalletConfig, - stake_account_keypair: &Keypair, - vote_account_pubkey: &Pubkey, - lamports: u64, - authorized: &Authorized, - force: bool, -) -> ProcessResult { - check_unique_pubkeys( - (&config.keypair.pubkey(), "wallet keypair".to_string()), - ( - &stake_account_keypair.pubkey(), - "stake_account_keypair".to_string(), - ), - )?; - - if rpc_client - .get_account(&stake_account_keypair.pubkey()) - .is_ok() - { - return Err(WalletError::BadParameter(format!( - "Unable to delegate. Stake account already exists: {}", - stake_account_keypair.pubkey() - )) - .into()); - } - - let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; - - let ixs = stake_instruction::create_stake_account_and_delegate_stake( - &config.keypair.pubkey(), - &stake_account_keypair.pubkey(), - vote_account_pubkey, - lamports, - authorized, - ); - - // Sanity check the vote account to ensure it is attached to a validator that has recently - // voted at the tip of the ledger - let vote_account_data = rpc_client - .get_account_data(vote_account_pubkey) - .map_err(|_| { - WalletError::RpcRequestError(format!("Vote account not found: {}", vote_account_pubkey)) - })?; - - let vote_state = VoteState::deserialize(&vote_account_data).map_err(|_| { - WalletError::RpcRequestError( - "Account data could not be deserialized to vote state".to_string(), - ) - })?; - - let sanity_check_result = match vote_state.root_slot { - None => Err(WalletError::BadParameter( - "Unable to delegate. Vote account has no root slot".to_string(), - )), - Some(root_slot) => { - let slot = rpc_client.get_slot()?; - if root_slot + solana_sdk::clock::DEFAULT_SLOTS_PER_TURN < slot { - Err(WalletError::BadParameter( - format!( - "Unable to delegate. Vote account root slot ({}) is too old, the current slot is {}", root_slot, slot - ) - )) - } else { - Ok(()) - } - } - }; - - if sanity_check_result.is_err() { - if !force { - sanity_check_result?; - } else { - println!("--force supplied, ignoring: {:?}", sanity_check_result); - } - } - - let mut tx = Transaction::new_signed_with_payer( - ixs, - Some(&config.keypair.pubkey()), - &[&config.keypair, &stake_account_keypair], - recent_blockhash, - ); - check_account_for_fee(rpc_client, config, &fee_calculator, &tx.message)?; - - let result = rpc_client - .send_and_confirm_transaction(&mut tx, &[&config.keypair, &stake_account_keypair]); - log_instruction_custom_error::(result) -} - -fn process_withdraw_stake( - rpc_client: &RpcClient, - config: &WalletConfig, - stake_account_keypair: &Keypair, - destination_account_pubkey: &Pubkey, - lamports: u64, -) -> ProcessResult { - let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; - let ixs = vec![stake_instruction::withdraw( - &stake_account_keypair.pubkey(), - destination_account_pubkey, - lamports, - )]; - - let mut tx = Transaction::new_signed_with_payer( - ixs, - Some(&config.keypair.pubkey()), - &[&config.keypair, &stake_account_keypair], - recent_blockhash, - ); - check_account_for_fee(rpc_client, config, &fee_calculator, &tx.message)?; - - let result = rpc_client - .send_and_confirm_transaction(&mut tx, &[&config.keypair, &stake_account_keypair]); - log_instruction_custom_error::(result) -} - -fn process_redeem_vote_credits( - rpc_client: &RpcClient, - config: &WalletConfig, - stake_account_pubkey: &Pubkey, - vote_account_pubkey: &Pubkey, -) -> ProcessResult { - let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; - let ixs = vec![stake_instruction::redeem_vote_credits( - stake_account_pubkey, - vote_account_pubkey, - )]; - let mut tx = Transaction::new_signed_with_payer( - ixs, - Some(&config.keypair.pubkey()), - &[&config.keypair], - recent_blockhash, - ); - check_account_for_fee(rpc_client, config, &fee_calculator, &tx.message)?; - let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair]); - log_instruction_custom_error::(result) -} - -fn process_show_stake_account( - rpc_client: &RpcClient, - _config: &WalletConfig, - stake_account_pubkey: &Pubkey, - use_lamports_unit: bool, -) -> ProcessResult { - use solana_stake_api::stake_state::StakeState; - let stake_account = rpc_client.get_account(stake_account_pubkey)?; - if stake_account.owner != solana_stake_api::id() { - Err(WalletError::RpcRequestError( - format!("{:?} is not a stake account", stake_account_pubkey).to_string(), - ))?; - } - fn show_authorized(authorized: &Authorized) { - println!("authorized staker: {}", authorized.staker); - println!("authorized withdrawer: {}", authorized.staker); - } - fn show_lockup(lockup: &Lockup) { - println!("lockup slot: {}", lockup.slot); - println!("lockup custodian: {}", lockup.custodian); - } - match stake_account.state() { - Ok(StakeState::Stake(authorized, lockup, stake)) => { - println!( - "total stake: {}", - build_balance_message(stake_account.lamports, use_lamports_unit) - ); - println!("credits observed: {}", stake.credits_observed); - println!( - "delegated stake: {}", - build_balance_message(stake.stake, use_lamports_unit) - ); - if stake.voter_pubkey != Pubkey::default() { - println!("delegated voter pubkey: {}", stake.voter_pubkey); - } - println!( - "stake activates starting from epoch: {}", - stake.activation_epoch - ); - if stake.deactivation_epoch < std::u64::MAX { - println!( - "stake deactivates starting from epoch: {}", - stake.deactivation_epoch - ); - } - show_authorized(&authorized); - show_lockup(&lockup); - Ok("".to_string()) - } - Ok(StakeState::RewardsPool) => Ok("Stake account is a rewards pool".to_string()), - Ok(StakeState::Uninitialized) => Ok("Stake account is uninitialized".to_string()), - Ok(StakeState::Initialized(authorized, lockup)) => { - println!("Stake account is undelegated"); - show_authorized(&authorized); - show_lockup(&lockup); - Ok("".to_string()) - } - Err(err) => Err(WalletError::RpcRequestError(format!( - "Account data could not be deserialized to stake state: {:?}", - err - )))?, - } -} - fn process_create_replicator_storage_account( rpc_client: &RpcClient, config: &WalletConfig, @@ -1374,14 +1099,12 @@ pub fn process_command(config: &WalletConfig) -> ProcessResult { WalletCommand::VoteAuthorize( vote_account_pubkey, - authorized_keypair, new_authorized_pubkey, vote_authorize, ) => process_vote_authorize( &rpc_client, config, &vote_account_pubkey, - &authorized_keypair, &new_authorized_pubkey, *vote_authorize, ), @@ -1414,40 +1137,56 @@ pub fn process_command(config: &WalletConfig) -> ProcessResult { span, } => process_uptime(&rpc_client, config, &vote_account_pubkey, *aggregate, *span), - WalletCommand::DelegateStake( - stake_account_keypair, - vote_account_pubkey, - lamports, - authorized, - force, - ) => process_delegate_stake( + // Create stake account + WalletCommand::CreateStakeAccount(stake_account_pubkey, authorized, lockup, lamports) => { + process_create_stake_account( + &rpc_client, + config, + &stake_account_pubkey, + &authorized, + lockup, + *lamports, + ) + } + WalletCommand::DelegateStake(stake_account_pubkey, vote_account_pubkey, force) => { + process_delegate_stake( + &rpc_client, + config, + &stake_account_pubkey, + &vote_account_pubkey, + *force, + ) + } + WalletCommand::StakeAuthorize( + stake_account_pubkey, + new_authorized_pubkey, + stake_authorize, + ) => process_stake_authorize( &rpc_client, config, - &stake_account_keypair, - &vote_account_pubkey, - *lamports, - &authorized, - *force, + &stake_account_pubkey, + &new_authorized_pubkey, + *stake_authorize, ), WalletCommand::WithdrawStake( - stake_account_keypair, + stake_account_pubkey, destination_account_pubkey, lamports, ) => process_withdraw_stake( &rpc_client, config, - &stake_account_keypair, + &stake_account_pubkey, &destination_account_pubkey, *lamports, ), // Deactivate stake account - WalletCommand::DeactivateStake(stake_account_keypair, vote_account_pubkey) => { + WalletCommand::DeactivateStake(stake_account_pubkey, vote_account_pubkey) => { process_deactivate_stake_account( &rpc_client, config, - &stake_account_keypair, + &stake_account_pubkey, &vote_account_pubkey, ) } @@ -1657,17 +1396,6 @@ pub(crate) fn build_balance_message(lamports: u64, use_lamports_unit: bool) -> S } } -pub(crate) fn parse_amount_lamports( - amount: &str, - use_lamports_unit: Option<&str>, -) -> Result> { - if use_lamports_unit.is_some() && use_lamports_unit.unwrap() == "lamports" { - Ok(amount.parse()?) - } else { - Ok(sol_to_lamports(amount.parse()?)) - } -} - pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, 'v> { App::new(name) .about(about) @@ -1704,6 +1432,7 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' .arg( Arg::with_name("unit") .index(2) + .value_name("UNIT") .takes_value(true) .possible_values(&["SOL", "lamports"]) .help("Specify unit to use for request and balance display"), @@ -1764,18 +1493,9 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' .validator(is_pubkey_or_keypair) .help("Vote account in which to set the authorized voter"), ) - .arg( - Arg::with_name("authorized_keypair_file") - .index(2) - .value_name("CURRENT VOTER KEYPAIR FILE") - .takes_value(true) - .required(true) - .validator(is_keypair) - .help("Keypair file for the currently authorized vote signer"), - ) .arg( Arg::with_name("new_authorized_pubkey") - .index(3) + .index(2) .value_name("NEW VOTER PUBKEY") .takes_value(true) .required(true) @@ -1795,18 +1515,9 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' .validator(is_pubkey_or_keypair) .help("Vote account in which to set the authorized withdrawer"), ) - .arg( - Arg::with_name("authorized_keypair_file") - .index(2) - .value_name("CURRENT WITHDRAWER KEYPAIR FILE") - .takes_value(true) - .required(true) - .validator(is_keypair) - .help("Keypair file for the currently authorized withdrawer"), - ) .arg( Arg::with_name("new_authorized_pubkey") - .index(3) + .index(2) .value_name("NEW WITHDRAWER PUBKEY") .takes_value(true) .required(true) @@ -1846,6 +1557,7 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' .arg( Arg::with_name("unit") .index(4) + .value_name("UNIT") .takes_value(true) .possible_values(&["SOL", "lamports"]) .help("Specify unit to use for request"), @@ -1863,7 +1575,7 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' .value_name("PUBKEY") .takes_value(true) .validator(is_pubkey_or_keypair) - .help("Public key of the authorized voter (defaults to vote account pubkey)"), + .help("Public key of the authorized voter (defaults to vote account)"), ) .arg( Arg::with_name("authorized_withdrawer") @@ -1871,10 +1583,8 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' .value_name("PUBKEY") .takes_value(true) .validator(is_pubkey_or_keypair) - .help("Public key of the authorized withdrawer (defaults to vote account pubkey)"), - ) - -, + .help("Public key of the authorized withdrawer (defaults to wallet)"), + ), ) .subcommand( SubCommand::with_name("show-account") @@ -1947,149 +1657,7 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' .help("Aggregate uptime data across span") ), ) - .subcommand( - SubCommand::with_name("delegate-stake") - .about("Delegate stake to a vote account") - .arg( - Arg::with_name("force") - .long("force") - .takes_value(false) - .hidden(true) // Don't document this argument to discourage its use - .help("Override vote account sanity checks (use carefully!)"), - ) - .arg( - Arg::with_name("stake_account_keypair_file") - .index(1) - .value_name("STAKE ACCOUNT KEYPAIR FILE") - .takes_value(true) - .required(true) - .validator(is_keypair) - .help("Keypair file for the new stake account"), - ) - .arg( - Arg::with_name("vote_account_pubkey") - .index(2) - .value_name("VOTE ACCOUNT PUBKEY") - .takes_value(true) - .required(true) - .validator(is_pubkey_or_keypair) - .help("The vote account to which the stake will be delegated"), - ) - .arg( - Arg::with_name("amount") - .index(3) - .value_name("AMOUNT") - .takes_value(true) - .required(true) - .help("The amount to delegate (default unit SOL)"), - ) - .arg( - Arg::with_name("unit") - .index(4) - .takes_value(true) - .possible_values(&["SOL", "lamports"]) - .help("Specify unit to use for request"), - ), - ) - .subcommand( - SubCommand::with_name("deactivate-stake") - .about("Deactivate the delegated stake from the stake account") - .arg( - Arg::with_name("stake_account_keypair_file") - .index(1) - .value_name("STAKE ACCOUNT KEYPAIR FILE") - .takes_value(true) - .required(true) - .help("Keypair file for the stake account, for signing the delegate transaction."), - ) - .arg( - Arg::with_name("vote_account_pubkey") - .index(2) - .value_name("PUBKEY") - .takes_value(true) - .required(true) - .validator(is_pubkey_or_keypair) - .help("The vote account to which the stake is currently delegated"), - ) - ) - .subcommand( - SubCommand::with_name("withdraw-stake") - .about("Withdraw the unstaked lamports from the stake account") - .arg( - Arg::with_name("stake_account_keypair_file") - .index(1) - .value_name("STAKE ACCOUNT KEYPAIR FILE") - .takes_value(true) - .required(true) - .validator(is_keypair) - .help("Keypair file for the stake account, for signing the withdraw transaction."), - ) - .arg( - Arg::with_name("destination_account_pubkey") - .index(2) - .value_name("DESTINATION PUBKEY") - .takes_value(true) - .required(true) - .validator(is_pubkey_or_keypair) - .help("The account where the lamports should be transfered"), - ) - .arg( - Arg::with_name("amount") - .index(3) - .value_name("AMOUNT") - .takes_value(true) - .required(true) - .help("The amount to withdraw from the stake account (default unit SOL)"), - ) - .arg( - Arg::with_name("unit") - .index(4) - .takes_value(true) - .possible_values(&["SOL", "lamports"]) - .help("Specify unit to use for request"), - ), - ) - .subcommand( - SubCommand::with_name("redeem-vote-credits") - .about("Redeem credits in the stake account") - .arg( - Arg::with_name("stake_account_pubkey") - .index(1) - .value_name("STAKING ACCOUNT PUBKEY") - .takes_value(true) - .required(true) - .validator(is_pubkey_or_keypair) - .help("Staking account address to redeem credits for"), - ) - .arg( - Arg::with_name("vote_account_pubkey") - .index(2) - .value_name("VOTE ACCOUNT PUBKEY") - .takes_value(true) - .required(true) - .validator(is_pubkey_or_keypair) - .help("The vote account to which the stake was previously delegated."), - ), - ) - .subcommand( - SubCommand::with_name("show-stake-account") - .about("Show the contents of a stake account") - .arg( - Arg::with_name("stake_account_pubkey") - .index(1) - .value_name("STAKE ACCOUNT PUBKEY") - .takes_value(true) - .required(true) - .validator(is_pubkey_or_keypair) - .help("Stake account pubkey"), - ) - .arg( - Arg::with_name("lamports") - .long("lamports") - .takes_value(false) - .help("Display balance in lamports instead of SOL"), - ), - ) + .stake_subcommands() .subcommand( SubCommand::with_name("create-storage-mining-pool-account") .about("Create mining pool account") @@ -2113,6 +1681,7 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' .arg( Arg::with_name("unit") .index(3) + .value_name("UNIT") .takes_value(true) .possible_values(&["SOL", "lamports"]) .help("Specify unit to use for request"), @@ -2243,6 +1812,7 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' .arg( Arg::with_name("unit") .index(3) + .value_name("UNIT") .takes_value(true) .possible_values(&["SOL", "lamports"]) .help("Specify unit to use for request"), @@ -2447,6 +2017,29 @@ mod tests { }; use std::path::PathBuf; + fn make_tmp_path(name: &str) -> String { + let out_dir = std::env::var("FARF_DIR").unwrap_or_else(|_| "farf".to_string()); + let keypair = Keypair::new(); + + let path = format!("{}/tmp/{}-{}", out_dir, name, keypair.pubkey()); + + // whack any possible collision + let _ignored = std::fs::remove_dir_all(&path); + // whack any possible collision + let _ignored = std::fs::remove_file(&path); + + path + } + + #[test] + #[should_panic] + fn test_bad_amount() { + let test_commands = app("test", "desc", "version"); + let test_bad_airdrop = test_commands.get_matches_from(vec!["test", "airdrop", "notint"]); + let pubkey = Pubkey::new_rand(); + let _ignored = parse_command(&pubkey, &test_bad_airdrop).unwrap(); + } + #[test] fn test_wallet_parse_command() { let test_commands = app("test", "desc", "version"); @@ -2472,10 +2065,6 @@ mod tests { use_lamports_unit: true, } ); - let test_bad_airdrop = test_commands - .clone() - .get_matches_from(vec!["test", "airdrop", "notint"]); - assert!(parse_command(&pubkey, &test_bad_airdrop).is_err()); // Test Balance Subcommand, incl pubkey and keypair-file inputs let keypair_file = make_tmp_path("keypair_file"); @@ -2533,97 +2122,6 @@ mod tests { .get_matches_from(vec!["test", "confirm", "deadbeef"]); assert!(parse_command(&pubkey, &test_bad_signature).is_err()); - // Test DelegateStake Subcommand - fn make_tmp_path(name: &str) -> String { - let out_dir = std::env::var("FARF_DIR").unwrap_or_else(|_| "farf".to_string()); - let keypair = Keypair::new(); - - let path = format!("{}/tmp/{}-{}", out_dir, name, keypair.pubkey()); - - // whack any possible collision - let _ignored = std::fs::remove_dir_all(&path); - // whack any possible collision - let _ignored = std::fs::remove_file(&path); - - path - } - - let keypair_file = make_tmp_path("keypair_file"); - gen_keypair_file(&keypair_file).unwrap(); - let keypair = read_keypair(&keypair_file).unwrap(); - - let test_delegate_stake = test_commands.clone().get_matches_from(vec![ - "test", - "delegate-stake", - &keypair_file, - &pubkey_string, - "42", - "lamports", - ]); - let stake_pubkey = keypair.pubkey(); - assert_eq!( - parse_command(&pubkey, &test_delegate_stake).unwrap(), - WalletCommand::DelegateStake( - keypair, - pubkey, - 42, - Authorized::auto(&stake_pubkey), - false, - ) - ); - - let keypair = read_keypair(&keypair_file).unwrap(); - let test_delegate_stake = test_commands.clone().get_matches_from(vec![ - "test", - "delegate-stake", - "--force", - &keypair_file, - &pubkey_string, - "42", - "lamports", - ]); - let stake_pubkey = keypair.pubkey(); - assert_eq!( - parse_command(&pubkey, &test_delegate_stake).unwrap(), - WalletCommand::DelegateStake( - keypair, - pubkey, - 42, - Authorized::auto(&stake_pubkey), - true - ) - ); - - // Test WithdrawStake Subcommand - let test_withdraw_stake = test_commands.clone().get_matches_from(vec![ - "test", - "withdraw-stake", - &keypair_file, - &pubkey_string, - "42", - "lamports", - ]); - let keypair = read_keypair(&keypair_file).unwrap(); - assert_eq!( - parse_command(&pubkey, &test_withdraw_stake).unwrap(), - WalletCommand::WithdrawStake(keypair, pubkey, 42) - ); - - // Test DeactivateStake Subcommand - let keypair_file = make_tmp_path("keypair_file"); - gen_keypair_file(&keypair_file).unwrap(); - let keypair = read_keypair(&keypair_file).unwrap(); - let test_deactivate_stake = test_commands.clone().get_matches_from(vec![ - "test", - "deactivate-stake", - &keypair_file, - &pubkey_string, - ]); - assert_eq!( - parse_command(&pubkey, &test_deactivate_stake).unwrap(), - WalletCommand::DeactivateStake(keypair, pubkey) - ); - // Test Deploy Subcommand let test_deploy = test_commands @@ -2831,36 +2329,35 @@ mod tests { let signature = process_command(&config); assert_eq!(signature.unwrap(), SIGNATURE.to_string()); - let bob_keypair = Keypair::new(); let new_authorized_pubkey = Pubkey::new_rand(); - config.command = WalletCommand::VoteAuthorize( + config.command = + WalletCommand::VoteAuthorize(bob_pubkey, new_authorized_pubkey, VoteAuthorize::Voter); + let signature = process_command(&config); + assert_eq!(signature.unwrap(), SIGNATURE.to_string()); + + let bob_pubkey = Pubkey::new_rand(); + let custodian = Pubkey::new_rand(); + config.command = WalletCommand::CreateStakeAccount( bob_pubkey, - bob_keypair, - new_authorized_pubkey, - VoteAuthorize::Voter, + Authorized { + staker: config.keypair.pubkey(), + withdrawer: config.keypair.pubkey(), + }, + Lockup { slot: 0, custodian }, + 10, ); let signature = process_command(&config); assert_eq!(signature.unwrap(), SIGNATURE.to_string()); - // TODO: Need to add mock GetAccountInfo to mock_rpc_client_request.rs to re-enable the - // DeactivateStake test. - /* - let bob_keypair = Keypair::new(); - let vote_pubkey = Pubkey::new_rand(); - config.command = WalletCommand::DelegateStake(bob_keypair.into(), vote_pubkey, 100, true); - let signature = process_command(&config); - assert_eq!(signature.unwrap(), SIGNATURE.to_string()); - */ - - let bob_keypair = Keypair::new(); + let stake_pubkey = Pubkey::new_rand(); let to_pubkey = Pubkey::new_rand(); - config.command = WalletCommand::WithdrawStake(bob_keypair.into(), to_pubkey, 100); + config.command = WalletCommand::WithdrawStake(stake_pubkey, to_pubkey, 100); let signature = process_command(&config); assert_eq!(signature.unwrap(), SIGNATURE.to_string()); - let bob_keypair = Keypair::new(); + let stake_pubkey = Pubkey::new_rand(); let vote_pubkey = Pubkey::new_rand(); - config.command = WalletCommand::DeactivateStake(bob_keypair.into(), vote_pubkey); + config.command = WalletCommand::DeactivateStake(stake_pubkey, vote_pubkey); let signature = process_command(&config); assert_eq!(signature.unwrap(), SIGNATURE.to_string()); @@ -3000,12 +2497,7 @@ mod tests { ); assert!(process_command(&config).is_err()); - config.command = WalletCommand::VoteAuthorize( - bob_pubkey, - Keypair::new(), - bob_pubkey, - VoteAuthorize::Voter, - ); + config.command = WalletCommand::VoteAuthorize(bob_pubkey, bob_pubkey, VoteAuthorize::Voter); assert!(process_command(&config).is_err()); config.command = WalletCommand::GetSlot; diff --git a/client/src/rpc_client.rs b/client/src/rpc_client.rs index 8b15f65717..c9f28d09da 100644 --- a/client/src/rpc_client.rs +++ b/client/src/rpc_client.rs @@ -311,8 +311,7 @@ impl RpcClient { response .and_then(|account_json| { - let account: Account = - serde_json::from_value(account_json).expect("deserialize account"); + let account: Account = serde_json::from_value(account_json)?; trace!("Response account {:?} {:?}", pubkey, account); Ok(account) }) diff --git a/core/src/staking_utils.rs b/core/src/staking_utils.rs index 49773a4a80..bc29d395cb 100644 --- a/core/src/staking_utils.rs +++ b/core/src/staking_utils.rs @@ -162,8 +162,8 @@ pub(crate) mod tests { &from_account.pubkey(), &stake_account_pubkey, vote_pubkey, - amount, &Authorized::auto(&stake_account_pubkey), + amount, ), ); } diff --git a/local_cluster/src/local_cluster.rs b/local_cluster/src/local_cluster.rs index db7edc8626..d24a3115bf 100644 --- a/local_cluster/src/local_cluster.rs +++ b/local_cluster/src/local_cluster.rs @@ -464,8 +464,8 @@ impl LocalCluster { &from_account.pubkey(), &stake_account_pubkey, &vote_account_pubkey, - amount, &StakeAuthorized::auto(&stake_account_pubkey), + amount, ), client.get_recent_blockhash().unwrap().0, ); diff --git a/multinode-demo/delegate-stake.sh b/multinode-demo/delegate-stake.sh index 185d5a35ed..84d3ac7b2a 100755 --- a/multinode-demo/delegate-stake.sh +++ b/multinode-demo/delegate-stake.sh @@ -104,5 +104,7 @@ set -x $solana_cli "${common_args[@]}" \ show-vote-account "$vote_keypair_path" $solana_cli "${common_args[@]}" \ - delegate-stake $maybe_force "$stake_keypair_path" "$vote_keypair_path" "$stake_lamports" lamports + create-stake-account $maybe_force "$stake_keypair_path" lamports +$solana_cli "${common_args[@]}" \ + delegate-stake $maybe_force "$stake_keypair_path" "$vote_keypair_path" $solana_cli "${common_args[@]}" show-stake-account "$stake_keypair_path" diff --git a/programs/stake_api/src/stake_instruction.rs b/programs/stake_api/src/stake_instruction.rs index 635925aed0..15e0ba0d80 100644 --- a/programs/stake_api/src/stake_instruction.rs +++ b/programs/stake_api/src/stake_instruction.rs @@ -110,9 +110,9 @@ pub enum StakeInstruction { pub fn create_stake_account_with_lockup( from_pubkey: &Pubkey, stake_pubkey: &Pubkey, - lamports: u64, authorized: &Authorized, lockup: &Lockup, + lamports: u64, ) -> Vec { vec![ system_instruction::create_account( @@ -133,15 +133,15 @@ pub fn create_stake_account_with_lockup( pub fn create_stake_account( from_pubkey: &Pubkey, stake_pubkey: &Pubkey, - lamports: u64, authorized: &Authorized, + lamports: u64, ) -> Vec { create_stake_account_with_lockup( from_pubkey, stake_pubkey, - lamports, authorized, &Lockup::default(), + lamports, ) } @@ -149,11 +149,15 @@ pub fn create_stake_account_and_delegate_stake( from_pubkey: &Pubkey, stake_pubkey: &Pubkey, vote_pubkey: &Pubkey, - lamports: u64, authorized: &Authorized, + lamports: u64, ) -> Vec { - let mut instructions = create_stake_account(from_pubkey, stake_pubkey, lamports, authorized); - instructions.push(delegate_stake(stake_pubkey, vote_pubkey)); + let mut instructions = create_stake_account(from_pubkey, stake_pubkey, authorized, lamports); + instructions.push(delegate_stake( + stake_pubkey, + &authorized.staker, + vote_pubkey, + )); instructions } @@ -206,32 +210,54 @@ pub fn redeem_vote_credits(stake_pubkey: &Pubkey, vote_pubkey: &Pubkey) -> Instr Instruction::new(id(), &StakeInstruction::RedeemVoteCredits, account_metas) } -pub fn delegate_stake(stake_pubkey: &Pubkey, vote_pubkey: &Pubkey) -> Instruction { - let account_metas = vec![ - AccountMeta::new(*stake_pubkey, true), - AccountMeta::new_credit_only(*vote_pubkey, false), - AccountMeta::new_credit_only(sysvar::clock::id(), false), - AccountMeta::new_credit_only(crate::config::id(), false), - ]; +pub fn delegate_stake( + stake_pubkey: &Pubkey, + authorized_pubkey: &Pubkey, + vote_pubkey: &Pubkey, +) -> Instruction { + let account_metas = metas_for_authorized_signer( + stake_pubkey, + authorized_pubkey, + &[ + AccountMeta::new_credit_only(*vote_pubkey, false), + AccountMeta::new_credit_only(sysvar::clock::id(), false), + AccountMeta::new_credit_only(crate::config::id(), false), + ], + ); Instruction::new(id(), &StakeInstruction::DelegateStake, account_metas) } -pub fn withdraw(stake_pubkey: &Pubkey, to_pubkey: &Pubkey, lamports: u64) -> Instruction { - let account_metas = vec![ - AccountMeta::new(*stake_pubkey, true), - AccountMeta::new_credit_only(*to_pubkey, false), - AccountMeta::new_credit_only(sysvar::clock::id(), false), - AccountMeta::new_credit_only(sysvar::stake_history::id(), false), - ]; +pub fn withdraw( + stake_pubkey: &Pubkey, + authorized_pubkey: &Pubkey, + to_pubkey: &Pubkey, + lamports: u64, +) -> Instruction { + let account_metas = metas_for_authorized_signer( + stake_pubkey, + authorized_pubkey, + &[ + AccountMeta::new_credit_only(*to_pubkey, false), + AccountMeta::new_credit_only(sysvar::clock::id(), false), + AccountMeta::new_credit_only(sysvar::stake_history::id(), false), + ], + ); Instruction::new(id(), &StakeInstruction::Withdraw(lamports), account_metas) } -pub fn deactivate_stake(stake_pubkey: &Pubkey, vote_pubkey: &Pubkey) -> Instruction { - let account_metas = vec![ - AccountMeta::new(*stake_pubkey, true), - AccountMeta::new_credit_only(*vote_pubkey, false), - AccountMeta::new_credit_only(sysvar::clock::id(), false), - ]; +pub fn deactivate_stake( + stake_pubkey: &Pubkey, + authorized_pubkey: &Pubkey, + vote_pubkey: &Pubkey, +) -> Instruction { + let account_metas = metas_for_authorized_signer( + stake_pubkey, + authorized_pubkey, + &[ + AccountMeta::new_credit_only(*vote_pubkey, false), + AccountMeta::new_credit_only(sysvar::clock::id(), false), + ], + ); Instruction::new(id(), &StakeInstruction::Deactivate, account_metas) } @@ -361,15 +387,28 @@ mod tests { Err(InstructionError::InvalidAccountData), ); assert_eq!( - process_instruction(&delegate_stake(&Pubkey::default(), &Pubkey::default())), + process_instruction(&delegate_stake( + &Pubkey::default(), + &Pubkey::default(), + &Pubkey::default() + )), Err(InstructionError::InvalidAccountData), ); assert_eq!( - process_instruction(&withdraw(&Pubkey::default(), &Pubkey::new_rand(), 100)), + process_instruction(&withdraw( + &Pubkey::default(), + &Pubkey::default(), + &Pubkey::default(), + 100 + )), Err(InstructionError::InvalidAccountData), ); assert_eq!( - process_instruction(&deactivate_stake(&Pubkey::default(), &Pubkey::default())), + process_instruction(&deactivate_stake( + &Pubkey::default(), + &Pubkey::default(), + &Pubkey::default() + )), Err(InstructionError::InvalidAccountData), ); } diff --git a/programs/stake_tests/tests/stake_instruction.rs b/programs/stake_tests/tests/stake_instruction.rs index f0b6b3f6f8..73da07bea4 100644 --- a/programs/stake_tests/tests/stake_instruction.rs +++ b/programs/stake_tests/tests/stake_instruction.rs @@ -101,8 +101,8 @@ fn test_stake_account_delegate() { &mint_pubkey, &staker_pubkey, &vote_pubkey, - 1_000_000, &authorized, + 1_000_000, )); bank_client .send_message(&[&mint_keypair, &staker_keypair], message) @@ -120,6 +120,7 @@ fn test_stake_account_delegate() { // Test that we cannot withdraw staked lamports let message = Message::new_with_payer( vec![stake_instruction::withdraw( + &staker_pubkey, &staker_pubkey, &Pubkey::new_rand(), 1_000_000, @@ -187,6 +188,7 @@ fn test_stake_account_delegate() { // Deactivate the stake let message = Message::new_with_payer( vec![stake_instruction::deactivate_stake( + &staker_pubkey, &staker_pubkey, &vote_pubkey, )], @@ -199,6 +201,7 @@ fn test_stake_account_delegate() { // Test that we cannot withdraw staked lamports due to cooldown period let message = Message::new_with_payer( vec![stake_instruction::withdraw( + &staker_pubkey, &staker_pubkey, &Pubkey::new_rand(), 1_000_000, @@ -220,6 +223,7 @@ fn test_stake_account_delegate() { let message = Message::new_with_payer( vec![stake_instruction::withdraw( + &staker_pubkey, &staker_pubkey, &Pubkey::new_rand(), 1_000_000, @@ -233,6 +237,7 @@ fn test_stake_account_delegate() { let message = Message::new_with_payer( vec![stake_instruction::withdraw( + &staker_pubkey, &staker_pubkey, &Pubkey::new_rand(), 250_000, @@ -257,6 +262,7 @@ fn test_stake_account_delegate() { // Test that we can withdraw now let message = Message::new_with_payer( vec![stake_instruction::withdraw( + &staker_pubkey, &staker_pubkey, &Pubkey::new_rand(), 750_000,