v0.23: backport cli refactoring and remote-wallet signing integration (#8487)

* CLI: dynamic signing reboot (#8384)

* Add keypair_util_from_path helper

* Cli: impl config.keypair as a trait object

* SDK: Add Debug and PartialEq for dyn Signer

* ClapUtils: Arg parsing from pubkey+signers to Presigner

* Impl Signers for &dyn Signer collections

* CLI: Add helper for getting signers from args

* CLI: Replace SigningAuthority with Signer trait-objs

* CLI: Drop disused signers command field

* CLI: Drop redundant tests

* Add clap validator that handles all current signer types

* clap_utils: Factor Presigner resolution to helper

* SDK: `From` for boxing Signer implementors to trait objects

* SDK: Derive `Clone` for `Presigner`

* Remove panic

* Cli: dedup signers in transfer for remote-wallet ergonomics

* Update docs vis-a-vis ASK changes

* Cli: update transaction types to use new dynamic-signer methods

* CLI: Fix tests No. 1

what to do about write_keypair outstanding

* Work around `CliConfig`'s signer not necessarily being a `Keypair`

* CLI: Fix tests No. 2

* Remove unused arg

* Remove unused methods

* Move offline arg constants upstream

* Make cli signing fallible

Co-authored-by: Trent Nelson <trent.a.b.nelson@gmail.com>

* Reinstate `create-stale-account` w/ seed test (#8401)

automerge

* CLI: collect and deduplicate signers (#8398)

* Rename (keypair util is not a thing)

* Add method to generate_unique_signers

* Cli: refactor signer handling and remote-wallet init

* Fixup unit tests

* Fixup intergation tests

* Update keypair path print statement

* Remove &None

* Use deterministic key in test

* Retain storage-account as index

* Make signer index-handling less brittle

* Cache pubkey on RemoteKeypair::new

* Make signer_of consistent + return pubkey

* Remove &matches double references

* Nonce authorities need special handling

* Make solana root key accessible on Ledger (#8421)

* Use 44/501 key as ledger id

* Add error codes

* Ledger key path rework (#8453)

automerge

* Ledger hardware wallet docs (#8472)

* Update protocol documentation

* Correct app-version command const

* Rough initial Ledger docs

* Add more docs

* Cleanup

* Add remote-wallet to docs TOC

Co-authored-by: Greg Fitzgerald <greg@solana.com>

* Add flag to confirm key on device

Co-authored-by: Trent Nelson <trent.a.b.nelson@gmail.com>
Co-authored-by: Greg Fitzgerald <greg@solana.com>
This commit is contained in:
Tyera Eulberg
2020-02-26 17:59:41 -07:00
committed by GitHub
parent 100a11f061
commit 1a4de4d3c4
33 changed files with 2895 additions and 2356 deletions

1
Cargo.lock generated
View File

@ -4319,6 +4319,7 @@ dependencies = [
"semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
"solana-sdk 0.23.7", "solana-sdk 0.23.7",
"thiserror 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", "thiserror 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
"url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]] [[package]]

View File

@ -3,6 +3,8 @@
* [Introduction](introduction.md) * [Introduction](introduction.md)
* [Using Solana from the Command-line](cli/README.md) * [Using Solana from the Command-line](cli/README.md)
* [Command-line Usage](cli/usage.md) * [Command-line Usage](cli/usage.md)
* [Remote Wallet](remote-wallet/README.md)
* [Ledger Hardware Wallet](remote-wallet/ledger.md)
* [Paper Wallet](paper-wallet/README.md) * [Paper Wallet](paper-wallet/README.md)
* [Installation](paper-wallet/installation.md) * [Installation](paper-wallet/installation.md)
* [Paper Wallet Usage](paper-wallet/usage.md) * [Paper Wallet Usage](paper-wallet/usage.md)

View File

@ -247,10 +247,10 @@ Refer to the following page for a comprehensive guide on running a validator:
Solana CLI tooling supports secure keypair input for stake delegation. To do so, Solana CLI tooling supports secure keypair input for stake delegation. To do so,
first create a stake account with some SOL. Use the special `ASK` keyword to first create a stake account with some SOL. Use the special `ASK` keyword to
trigger a seed phrase input prompt for the stake account and use trigger a seed phrase input prompt for the stake account and use
`--ask-seed-phrase keypair` to securely input the funding keypair. `--keypair ASK` to securely input the funding keypair.
```bash ```bash
solana create-stake-account ASK 1 --ask-seed-phrase keypair solana create-stake-account ASK 1 --keypair ASK
[stake_account] seed phrase: 🔒 [stake_account] seed phrase: 🔒
[stake_account] If this seed phrase has an associated passphrase, enter it now. Otherwise, press ENTER to continue: [stake_account] If this seed phrase has an associated passphrase, enter it now. Otherwise, press ENTER to continue:
@ -258,11 +258,11 @@ solana create-stake-account ASK 1 --ask-seed-phrase keypair
[keypair] If this seed phrase has an associated passphrase, enter it now. Otherwise, press ENTER to continue: [keypair] If this seed phrase has an associated passphrase, enter it now. Otherwise, press ENTER to continue:
``` ```
Then, to delegate that stake to a validator, use `--ask-seed-phrase keypair` to Then, to delegate that stake to a validator, use `--keypair ASK` to
securely input the funding keypair. securely input the funding keypair.
```bash ```bash
solana delegate-stake --ask-seed-phrase keypair <STAKE_ACCOUNT_PUBKEY> <VOTE_ACCOUNT_PUBKEY> solana delegate-stake --keypair ASK <STAKE_ACCOUNT_PUBKEY> <VOTE_ACCOUNT_PUBKEY>
[keypair] seed phrase: 🔒 [keypair] seed phrase: 🔒
[keypair] If this seed phrase has an associated passphrase, enter it now. Otherwise, press ENTER to continue: [keypair] If this seed phrase has an associated passphrase, enter it now. Otherwise, press ENTER to continue:

View File

@ -0,0 +1,12 @@
# Remote Wallet
This document describes how to use a remote wallet with the Solana CLI
tools. Currently, Solana supports:
- Ledger hardware wallet (Nano S)
## Overview
Solana's remote-wallet integration provides Solana CLI methods to query a
device's BIP32 public keys and send messages to the device for secure signing.
{% page-ref page="ledger.md" %}

View File

@ -0,0 +1,132 @@
# Ledger Hardware Wallet
The Ledger Nano S hardware wallet offers secure storage of your Solana private
keys. The Solana Ledger app enables derivation of essentially infinite keys, and
secure transaction signing.
## Before You Begin
- [Initialize your Ledger Nano S](https://support.ledger.com/hc/en-us/articles/360000613793)
- [Install the latest device firmware](https://support.ledgerwallet.com/hc/en-us/articles/360002731113-Update-Ledger-Nano-S-firmware)
- [Install Ledger Live](https://support.ledger.com/hc/en-us/articles/360006395553/) software on your computer
## Install the Solana App on Ledger Nano S
1. Open the Manager in Ledger Live
2. Connect and unlock your Ledger Nano S
3. If asked, allow the manager on your device by pressing the right button
4. Find Solana in the app catalog and click Install
5. An installation window appears and your device will display Processing…
6. The app installation is confirmed
## Use Ledger Device with Solana CLI
1. Plug your Ledger device into your computer's USB port
2. Enter your pin and start the Solana app on the Ledger device
3. On your computer, run:
```text
solana address --keypair usb://ledger
```
This confirms your Ledger device is connected properly and in the correct state
to interact with the Solana CLI. The command returns your Ledger's unique
*wallet key*. When you have multiple Nano S devices connected to the same
computer, you can use your wallet key to specify which Ledger hardware wallet
you want to use. Run the same command again, but this time, with its fully
qualified URL:
```text
solana address --keypair usb://ledger/nano-s/<WALLET_KEY>
```
Confirm it prints the same key as when you entered just `usb://ledger`.
### Ledger Device URLs
Solana defines a format for the URL protocol "usb://" to uniquely locate any Solana key on
any remote wallet connected to your computer.
The URL has the form, where square brackets denote optional fields:
```text
usb://ledger[/<LEDGER_TYPE>[/<WALLET_KEY>]][?key=<DERIVATION_PATH>]
```
`LEDGER_TYPE` is optional and defaults to the value "nano-s". If the value is provided,
it must be "nano-s" without quotes, the only supported Ledger device at this time.
`WALLET_KEY` is used to disambiguate multiple Nano S devices. Every Ledger has
a unique master key and from that key derives a separate unique key per app.
`DERVIATION_PATH` is used to navigate to Solana keys within your Ledger hardware
wallet. The path has the form `<ACCOUNT>[/<CHANGE>]`, where each `ACCOUNT` and
`CHANGE` are postive integers.
All derivation paths implicitly include the prefix `44'/501'`, which indicates
the path follows the [BIP44 specifications](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki)
and that any dervied keys are Solana keys (Coin type 501). The single quote
indicates a "hardened" derivation. Because Solana uses Ed25519 keypairs, all
derivations are hardened and therefore adding the quote is optional and
unnecessary.
For example, a complete Ledger device path might be:
```text
usb://ledger/nano-s/BsNsvfXqQTtJnagwFWdBS7FBXgnsK8VZ5CmuznN85swK?key=0/0
```
### Set CLI Configuration
If you want to set a Ledger key as the default signer for CLI commands, use the
[CLI configuration settings](../cli/usage.md#solana-config):
```text
solana config set --keypair <LEDGER_URL>
```
For example:
```text
solana config set --keypair usb://ledger?key=0
```
### Check Account Balance
```text
solana balance --keypair usb://ledger?key=12345
```
Or with the default signer:
```text
solana balance
```
### Send SOL via Ledger Device
```text
solana transfer <RECIPIENT> <AMOUNT> --from <LEDGER_URL>
```
Or with the default signer:
```text
solana transfer <RECIPIENT> <AMOUNT>
```
### Delegate Stake with Ledger Device
```text
solana delegate-stake <STAKE_ACCOUNT> <VOTER_ID> --keypair <LEDGER_URL>
```
Or with the default signer:
```text
solana delegate-stake <STAKE_ACCOUNT> <VOTER_ID>
```
## Support
Email maintainers@solana.com

View File

@ -1,14 +1,16 @@
use crate::keypair::{keypair_from_seed_phrase, ASK_KEYWORD, SKIP_SEED_PHRASE_VALIDATION_ARG}; use crate::keypair::{
keypair_from_seed_phrase, signer_from_path, ASK_KEYWORD, SKIP_SEED_PHRASE_VALIDATION_ARG,
};
use chrono::DateTime; use chrono::DateTime;
use clap::ArgMatches; use clap::ArgMatches;
use solana_remote_wallet::remote_wallet::DerivationPath; use solana_remote_wallet::remote_wallet::{DerivationPath, RemoteWalletManager};
use solana_sdk::{ use solana_sdk::{
clock::UnixTimestamp, clock::UnixTimestamp,
native_token::sol_to_lamports, native_token::sol_to_lamports,
pubkey::Pubkey, pubkey::Pubkey,
signature::{read_keypair_file, Keypair, Signature, Signer}, signature::{read_keypair_file, Keypair, Signature, Signer},
}; };
use std::str::FromStr; use std::{str::FromStr, sync::Arc};
// Return parsed values from matches at `name` // Return parsed values from matches at `name`
pub fn values_of<T>(matches: &ArgMatches<'_>, name: &str) -> Option<Vec<T>> pub fn values_of<T>(matches: &ArgMatches<'_>, name: &str) -> Option<Vec<T>>
@ -93,6 +95,22 @@ pub fn pubkeys_sigs_of(matches: &ArgMatches<'_>, name: &str) -> Option<Vec<(Pubk
}) })
} }
// Return a signer from matches at `name`
#[allow(clippy::type_complexity)]
pub fn signer_of(
matches: &ArgMatches<'_>,
name: &str,
wallet_manager: Option<&Arc<RemoteWalletManager>>,
) -> Result<(Option<Box<dyn Signer>>, Option<Pubkey>), Box<dyn std::error::Error>> {
if let Some(location) = matches.value_of(name) {
let signer = signer_from_path(matches, location, name, wallet_manager)?;
let signer_pubkey = signer.pubkey();
Ok((Some(signer), Some(signer_pubkey)))
} else {
Ok((None, None))
}
}
pub fn lamports_of_sol(matches: &ArgMatches<'_>, name: &str) -> Option<u64> { pub fn lamports_of_sol(matches: &ArgMatches<'_>, name: &str) -> Option<u64> {
value_of(matches, name).map(sol_to_lamports) value_of(matches, name).map(sol_to_lamports)
} }
@ -101,8 +119,8 @@ pub fn derivation_of(matches: &ArgMatches<'_>, name: &str) -> Option<DerivationP
matches.value_of(name).map(|derivation_str| { matches.value_of(name).map(|derivation_str| {
let derivation_str = derivation_str.replace("'", ""); let derivation_str = derivation_str.replace("'", "");
let mut parts = derivation_str.split('/'); let mut parts = derivation_str.split('/');
let account = parts.next().unwrap().parse::<u16>().unwrap(); let account = parts.next().map(|account| account.parse::<u32>().unwrap());
let change = parts.next().map(|change| change.parse::<u16>().unwrap()); let change = parts.next().map(|change| change.parse::<u32>().unwrap());
DerivationPath { account, change } DerivationPath { account, change }
}) })
} }
@ -290,7 +308,7 @@ mod tests {
assert_eq!( assert_eq!(
derivation_of(&matches, "single"), derivation_of(&matches, "single"),
Some(DerivationPath { Some(DerivationPath {
account: 2, account: Some(2),
change: Some(3) change: Some(3)
}) })
); );
@ -301,7 +319,7 @@ mod tests {
assert_eq!( assert_eq!(
derivation_of(&matches, "single"), derivation_of(&matches, "single"),
Some(DerivationPath { Some(DerivationPath {
account: 2, account: Some(2),
change: None change: None
}) })
); );
@ -312,7 +330,7 @@ mod tests {
assert_eq!( assert_eq!(
derivation_of(&matches, "single"), derivation_of(&matches, "single"),
Some(DerivationPath { Some(DerivationPath {
account: 2, account: Some(2),
change: Some(3) change: Some(3)
}) })
); );

View File

@ -1,4 +1,4 @@
use crate::keypair::ASK_KEYWORD; use crate::keypair::{parse_keypair_path, KeypairUrl, ASK_KEYWORD};
use chrono::DateTime; use chrono::DateTime;
use solana_sdk::{ use solana_sdk::{
hash::Hash, hash::Hash,
@ -50,6 +50,13 @@ pub fn is_pubkey_or_keypair_or_ask_keyword(string: String) -> Result<(), String>
is_pubkey(string.clone()).or_else(|_| is_keypair_or_ask_keyword(string)) is_pubkey(string.clone()).or_else(|_| is_keypair_or_ask_keyword(string))
} }
pub fn is_valid_signer(string: String) -> Result<(), String> {
match parse_keypair_path(&string) {
KeypairUrl::Filepath(path) => is_keypair(path),
_ => Ok(()),
}
}
// Return an error if string cannot be parsed as pubkey=signature string // Return an error if string cannot be parsed as pubkey=signature string
pub fn is_pubkey_sig(string: String) -> Result<(), String> { pub fn is_pubkey_sig(string: String) -> Result<(), String> {
let mut signer = string.split('='); let mut signer = string.split('=');
@ -135,7 +142,7 @@ pub fn is_derivation(value: String) -> Result<(), String> {
let mut parts = value.split('/'); let mut parts = value.split('/');
let account = parts.next().unwrap(); let account = parts.next().unwrap();
account account
.parse::<u16>() .parse::<u32>()
.map_err(|e| { .map_err(|e| {
format!( format!(
"Unable to parse derivation, provided: {}, err: {:?}", "Unable to parse derivation, provided: {}, err: {:?}",
@ -144,7 +151,7 @@ pub fn is_derivation(value: String) -> Result<(), String> {
}) })
.and_then(|_| { .and_then(|_| {
if let Some(change) = parts.next() { if let Some(change) = parts.next() {
change.parse::<u16>().map_err(|e| { change.parse::<u32>().map_err(|e| {
format!( format!(
"Unable to parse derivation, provided: {}, err: {:?}", "Unable to parse derivation, provided: {}, err: {:?}",
change, e change, e
@ -165,11 +172,12 @@ mod tests {
fn test_is_derivation() { fn test_is_derivation() {
assert_eq!(is_derivation("2".to_string()), Ok(())); assert_eq!(is_derivation("2".to_string()), Ok(()));
assert_eq!(is_derivation("0".to_string()), Ok(())); assert_eq!(is_derivation("0".to_string()), Ok(()));
assert_eq!(is_derivation("65537".to_string()), Ok(()));
assert_eq!(is_derivation("0/2".to_string()), Ok(())); assert_eq!(is_derivation("0/2".to_string()), Ok(()));
assert_eq!(is_derivation("0'/2'".to_string()), Ok(())); assert_eq!(is_derivation("0'/2'".to_string()), Ok(()));
assert!(is_derivation("a".to_string()).is_err()); assert!(is_derivation("a".to_string()).is_err());
assert!(is_derivation("65537".to_string()).is_err()); assert!(is_derivation("4294967296".to_string()).is_err());
assert!(is_derivation("a/b".to_string()).is_err()); assert!(is_derivation("a/b".to_string()).is_err());
assert!(is_derivation("0/65537".to_string()).is_err()); assert!(is_derivation("0/4294967296".to_string()).is_err());
} }
} }

View File

@ -1,14 +1,28 @@
use crate::ArgConstant; use crate::{
input_parsers::{derivation_of, pubkeys_sigs_of},
offline::SIGNER_ARG,
ArgConstant,
};
use bip39::{Language, Mnemonic, Seed}; use bip39::{Language, Mnemonic, Seed};
use clap::values_t; use clap::{values_t, ArgMatches, Error, ErrorKind};
use rpassword::prompt_password_stderr; use rpassword::prompt_password_stderr;
use solana_sdk::signature::{ use solana_remote_wallet::{
keypair_from_seed, keypair_from_seed_phrase_and_passphrase, read_keypair_file, Keypair, Signer, remote_keypair::generate_remote_keypair,
remote_wallet::{RemoteWalletError, RemoteWalletManager},
};
use solana_sdk::{
pubkey::Pubkey,
signature::{
keypair_from_seed, keypair_from_seed_phrase_and_passphrase, read_keypair,
read_keypair_file, Keypair, Presigner, Signature, Signer,
},
}; };
use std::{ use std::{
error, error,
io::{stdin, stdout, Write}, io::{stdin, stdout, Write},
process::exit, process::exit,
str::FromStr,
sync::Arc,
}; };
pub enum KeypairUrl { pub enum KeypairUrl {
@ -16,6 +30,7 @@ pub enum KeypairUrl {
Filepath(String), Filepath(String),
Usb(String), Usb(String),
Stdin, Stdin,
Pubkey(Pubkey),
} }
pub fn parse_keypair_path(path: &str) -> KeypairUrl { pub fn parse_keypair_path(path: &str) -> KeypairUrl {
@ -24,12 +39,76 @@ pub fn parse_keypair_path(path: &str) -> KeypairUrl {
} else if path == ASK_KEYWORD { } else if path == ASK_KEYWORD {
KeypairUrl::Ask KeypairUrl::Ask
} else if path.starts_with("usb://") { } else if path.starts_with("usb://") {
KeypairUrl::Usb(path.split_at(6).1.to_string()) KeypairUrl::Usb(path.to_string())
} else if let Ok(pubkey) = Pubkey::from_str(path) {
KeypairUrl::Pubkey(pubkey)
} else { } else {
KeypairUrl::Filepath(path.to_string()) KeypairUrl::Filepath(path.to_string())
} }
} }
pub fn presigner_from_pubkey_sigs(
pubkey: &Pubkey,
signers: &[(Pubkey, Signature)],
) -> Option<Presigner> {
signers.iter().find_map(|(signer, sig)| {
if *signer == *pubkey {
Some(Presigner::new(signer, sig))
} else {
None
}
})
}
pub fn signer_from_path(
matches: &ArgMatches,
path: &str,
keypair_name: &str,
wallet_manager: Option<&Arc<RemoteWalletManager>>,
) -> Result<Box<dyn Signer>, Box<dyn error::Error>> {
match parse_keypair_path(path) {
KeypairUrl::Ask => {
let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name);
Ok(Box::new(keypair_from_seed_phrase(
keypair_name,
skip_validation,
false,
)?))
}
KeypairUrl::Filepath(path) => Ok(Box::new(read_keypair_file(&path)?)),
KeypairUrl::Stdin => {
let mut stdin = std::io::stdin();
Ok(Box::new(read_keypair(&mut stdin)?))
}
KeypairUrl::Usb(path) => {
if let Some(wallet_manager) = wallet_manager {
Ok(Box::new(generate_remote_keypair(
path,
derivation_of(matches, "derivation_path"),
wallet_manager,
matches.is_present("confirm_key"),
)?))
} else {
Err(RemoteWalletError::NoDeviceFound.into())
}
}
KeypairUrl::Pubkey(pubkey) => {
let presigner = pubkeys_sigs_of(matches, SIGNER_ARG.name)
.as_ref()
.and_then(|presigners| presigner_from_pubkey_sigs(&pubkey, presigners));
if let Some(presigner) = presigner {
Ok(Box::new(presigner))
} else {
Err(Error::with_description(
"Missing signature for supplied pubkey",
ErrorKind::MissingRequiredArgument,
)
.into())
}
}
}
}
// Keyword used to indicate that the user should be asked for a keypair seed phrase // Keyword used to indicate that the user should be asked for a keypair seed phrase
pub const ASK_KEYWORD: &str = "ASK"; pub const ASK_KEYWORD: &str = "ASK";

View File

@ -26,3 +26,4 @@ pub struct ArgConstant<'a> {
pub mod input_parsers; pub mod input_parsers;
pub mod input_validators; pub mod input_validators;
pub mod keypair; pub mod keypair;
pub mod offline;

19
clap-utils/src/offline.rs Normal file
View File

@ -0,0 +1,19 @@
use crate::ArgConstant;
pub const BLOCKHASH_ARG: ArgConstant<'static> = ArgConstant {
name: "blockhash",
long: "blockhash",
help: "Use the supplied blockhash",
};
pub const SIGN_ONLY_ARG: ArgConstant<'static> = ArgConstant {
name: "sign_only",
long: "sign-only",
help: "Sign the transaction offline",
};
pub const SIGNER_ARG: ArgConstant<'static> = ArgConstant {
name: "signer",
long: "signer",
help: "Provide a public-key/signature pair for the transaction",
};

File diff suppressed because it is too large Load Diff

View File

@ -8,21 +8,25 @@ use crate::{
use clap::{value_t, value_t_or_exit, App, Arg, ArgMatches, SubCommand}; use clap::{value_t, value_t_or_exit, App, Arg, ArgMatches, SubCommand};
use console::{style, Emoji}; use console::{style, Emoji};
use indicatif::{ProgressBar, ProgressStyle}; use indicatif::{ProgressBar, ProgressStyle};
use solana_clap_utils::{input_parsers::*, input_validators::*}; use solana_clap_utils::{input_parsers::*, input_validators::*, keypair::signer_from_path};
use solana_client::{rpc_client::RpcClient, rpc_response::RpcVoteAccountInfo}; use solana_client::{rpc_client::RpcClient, rpc_response::RpcVoteAccountInfo};
use solana_remote_wallet::remote_wallet::RemoteWalletManager;
use solana_sdk::{ use solana_sdk::{
account_utils::StateMut, account_utils::StateMut,
clock::{self, Slot}, clock::{self, Slot},
commitment_config::CommitmentConfig, commitment_config::CommitmentConfig,
epoch_schedule::Epoch, epoch_schedule::Epoch,
hash::Hash, hash::Hash,
message::Message,
pubkey::Pubkey, pubkey::Pubkey,
signature::{Keypair, Signer}, signature::{Keypair, Signer},
system_transaction, system_instruction,
transaction::Transaction,
}; };
use std::{ use std::{
collections::{HashMap, VecDeque}, collections::{HashMap, VecDeque},
net::SocketAddr, net::SocketAddr,
sync::Arc,
thread::sleep, thread::sleep,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
@ -216,11 +220,15 @@ pub fn parse_catchup(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliErro
let node_pubkey = pubkey_of(matches, "node_pubkey").unwrap(); let node_pubkey = pubkey_of(matches, "node_pubkey").unwrap();
Ok(CliCommandInfo { Ok(CliCommandInfo {
command: CliCommand::Catchup { node_pubkey }, command: CliCommand::Catchup { node_pubkey },
require_keypair: false, signers: vec![],
}) })
} }
pub fn parse_cluster_ping(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliError> { pub fn parse_cluster_ping(
matches: &ArgMatches<'_>,
default_signer_path: &str,
wallet_manager: Option<&Arc<RemoteWalletManager>>,
) -> Result<CliCommandInfo, CliError> {
let lamports = value_t_or_exit!(matches, "lamports", u64); let lamports = value_t_or_exit!(matches, "lamports", u64);
let interval = Duration::from_secs(value_t_or_exit!(matches, "interval", u64)); let interval = Duration::from_secs(value_t_or_exit!(matches, "interval", u64));
let count = if matches.is_present("count") { let count = if matches.is_present("count") {
@ -242,7 +250,12 @@ pub fn parse_cluster_ping(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, Cl
timeout, timeout,
commitment_config, commitment_config,
}, },
require_keypair: true, signers: vec![signer_from_path(
matches,
default_signer_path,
"keypair",
wallet_manager,
)?],
}) })
} }
@ -250,7 +263,7 @@ pub fn parse_get_block_time(matches: &ArgMatches<'_>) -> Result<CliCommandInfo,
let slot = value_t_or_exit!(matches, "slot", u64); let slot = value_t_or_exit!(matches, "slot", u64);
Ok(CliCommandInfo { Ok(CliCommandInfo {
command: CliCommand::GetBlockTime { slot }, command: CliCommand::GetBlockTime { slot },
require_keypair: false, signers: vec![],
}) })
} }
@ -262,7 +275,7 @@ pub fn parse_get_epoch_info(matches: &ArgMatches<'_>) -> Result<CliCommandInfo,
}; };
Ok(CliCommandInfo { Ok(CliCommandInfo {
command: CliCommand::GetEpochInfo { commitment_config }, command: CliCommand::GetEpochInfo { commitment_config },
require_keypair: false, signers: vec![],
}) })
} }
@ -274,7 +287,7 @@ pub fn parse_get_slot(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliErr
}; };
Ok(CliCommandInfo { Ok(CliCommandInfo {
command: CliCommand::GetSlot { commitment_config }, command: CliCommand::GetSlot { commitment_config },
require_keypair: false, signers: vec![],
}) })
} }
@ -286,7 +299,7 @@ pub fn parse_get_transaction_count(matches: &ArgMatches<'_>) -> Result<CliComman
}; };
Ok(CliCommandInfo { Ok(CliCommandInfo {
command: CliCommand::GetTransactionCount { commitment_config }, command: CliCommand::GetTransactionCount { commitment_config },
require_keypair: false, signers: vec![],
}) })
} }
@ -299,7 +312,7 @@ pub fn parse_show_stakes(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, Cli
use_lamports_unit, use_lamports_unit,
vote_account_pubkeys, vote_account_pubkeys,
}, },
require_keypair: false, signers: vec![],
}) })
} }
@ -308,7 +321,7 @@ pub fn parse_show_validators(matches: &ArgMatches<'_>) -> Result<CliCommandInfo,
Ok(CliCommandInfo { Ok(CliCommandInfo {
command: CliCommand::ShowValidators { use_lamports_unit }, command: CliCommand::ShowValidators { use_lamports_unit },
require_keypair: false, signers: vec![],
}) })
} }
@ -500,7 +513,7 @@ pub fn parse_show_block_production(matches: &ArgMatches<'_>) -> Result<CliComman
Ok(CliCommandInfo { Ok(CliCommandInfo {
command: CliCommand::ShowBlockProduction { epoch, slot_limit }, command: CliCommand::ShowBlockProduction { epoch, slot_limit },
require_keypair: false, signers: vec![],
}) })
} }
@ -695,7 +708,7 @@ pub fn process_ping(
) -> ProcessResult { ) -> ProcessResult {
let to = Keypair::new().pubkey(); let to = Keypair::new().pubkey();
println_name_value("Source Account:", &config.keypair.pubkey().to_string()); println_name_value("Source Account:", &config.signers[0].pubkey().to_string());
println_name_value("Destination Account:", &to.to_string()); println_name_value("Destination Account:", &to.to_string());
println!(); println!();
@ -714,11 +727,13 @@ pub fn process_ping(
let (recent_blockhash, fee_calculator) = rpc_client.get_new_blockhash(&last_blockhash)?; let (recent_blockhash, fee_calculator) = rpc_client.get_new_blockhash(&last_blockhash)?;
last_blockhash = recent_blockhash; last_blockhash = recent_blockhash;
let transaction = let ix = system_instruction::transfer(&config.signers[0].pubkey(), &to, lamports);
system_transaction::transfer(&config.keypair, &to, lamports, recent_blockhash); let message = Message::new(vec![ix]);
let mut transaction = Transaction::new_unsigned(message);
transaction.try_sign(&config.signers, recent_blockhash)?;
check_account_for_fee( check_account_for_fee(
rpc_client, rpc_client,
&config.keypair.pubkey(), &config.signers[0].pubkey(),
&fee_calculator, &fee_calculator,
&transaction.message, &transaction.message,
)?; )?;
@ -1015,28 +1030,38 @@ pub fn process_show_validators(rpc_client: &RpcClient, use_lamports_unit: bool)
mod tests { mod tests {
use super::*; use super::*;
use crate::cli::{app, parse_command}; use crate::cli::{app, parse_command};
use solana_sdk::signature::{write_keypair, Keypair};
use tempfile::NamedTempFile;
fn make_tmp_file() -> (String, NamedTempFile) {
let tmp_file = NamedTempFile::new().unwrap();
(String::from(tmp_file.path().to_str().unwrap()), tmp_file)
}
#[test] #[test]
fn test_parse_command() { fn test_parse_command() {
let test_commands = app("test", "desc", "version"); let test_commands = app("test", "desc", "version");
let default_keypair = Keypair::new();
let (default_keypair_file, mut tmp_file) = make_tmp_file();
write_keypair(&default_keypair, tmp_file.as_file_mut()).unwrap();
let test_cluster_version = test_commands let test_cluster_version = test_commands
.clone() .clone()
.get_matches_from(vec!["test", "cluster-version"]); .get_matches_from(vec!["test", "cluster-version"]);
assert_eq!( assert_eq!(
parse_command(&test_cluster_version).unwrap(), parse_command(&test_cluster_version, &default_keypair_file, None).unwrap(),
CliCommandInfo { CliCommandInfo {
command: CliCommand::ClusterVersion, command: CliCommand::ClusterVersion,
require_keypair: false signers: vec![],
} }
); );
let test_fees = test_commands.clone().get_matches_from(vec!["test", "fees"]); let test_fees = test_commands.clone().get_matches_from(vec!["test", "fees"]);
assert_eq!( assert_eq!(
parse_command(&test_fees).unwrap(), parse_command(&test_fees, &default_keypair_file, None).unwrap(),
CliCommandInfo { CliCommandInfo {
command: CliCommand::Fees, command: CliCommand::Fees,
require_keypair: false signers: vec![],
} }
); );
@ -1046,10 +1071,10 @@ mod tests {
.clone() .clone()
.get_matches_from(vec!["test", "block-time", &slot.to_string()]); .get_matches_from(vec!["test", "block-time", &slot.to_string()]);
assert_eq!( assert_eq!(
parse_command(&test_get_block_time).unwrap(), parse_command(&test_get_block_time, &default_keypair_file, None).unwrap(),
CliCommandInfo { CliCommandInfo {
command: CliCommand::GetBlockTime { slot }, command: CliCommand::GetBlockTime { slot },
require_keypair: false signers: vec![],
} }
); );
@ -1057,12 +1082,12 @@ mod tests {
.clone() .clone()
.get_matches_from(vec!["test", "epoch-info"]); .get_matches_from(vec!["test", "epoch-info"]);
assert_eq!( assert_eq!(
parse_command(&test_get_epoch_info).unwrap(), parse_command(&test_get_epoch_info, &default_keypair_file, None).unwrap(),
CliCommandInfo { CliCommandInfo {
command: CliCommand::GetEpochInfo { command: CliCommand::GetEpochInfo {
commitment_config: CommitmentConfig::recent(), commitment_config: CommitmentConfig::recent(),
}, },
require_keypair: false signers: vec![],
} }
); );
@ -1070,21 +1095,21 @@ mod tests {
.clone() .clone()
.get_matches_from(vec!["test", "genesis-hash"]); .get_matches_from(vec!["test", "genesis-hash"]);
assert_eq!( assert_eq!(
parse_command(&test_get_genesis_hash).unwrap(), parse_command(&test_get_genesis_hash, &default_keypair_file, None).unwrap(),
CliCommandInfo { CliCommandInfo {
command: CliCommand::GetGenesisHash, command: CliCommand::GetGenesisHash,
require_keypair: false signers: vec![],
} }
); );
let test_get_slot = test_commands.clone().get_matches_from(vec!["test", "slot"]); let test_get_slot = test_commands.clone().get_matches_from(vec!["test", "slot"]);
assert_eq!( assert_eq!(
parse_command(&test_get_slot).unwrap(), parse_command(&test_get_slot, &default_keypair_file, None).unwrap(),
CliCommandInfo { CliCommandInfo {
command: CliCommand::GetSlot { command: CliCommand::GetSlot {
commitment_config: CommitmentConfig::recent(), commitment_config: CommitmentConfig::recent(),
}, },
require_keypair: false signers: vec![],
} }
); );
@ -1092,12 +1117,12 @@ mod tests {
.clone() .clone()
.get_matches_from(vec!["test", "transaction-count"]); .get_matches_from(vec!["test", "transaction-count"]);
assert_eq!( assert_eq!(
parse_command(&test_transaction_count).unwrap(), parse_command(&test_transaction_count, &default_keypair_file, None).unwrap(),
CliCommandInfo { CliCommandInfo {
command: CliCommand::GetTransactionCount { command: CliCommand::GetTransactionCount {
commitment_config: CommitmentConfig::recent(), commitment_config: CommitmentConfig::recent(),
}, },
require_keypair: false signers: vec![],
} }
); );
@ -1113,7 +1138,7 @@ mod tests {
"--confirmed", "--confirmed",
]); ]);
assert_eq!( assert_eq!(
parse_command(&test_ping).unwrap(), parse_command(&test_ping, &default_keypair_file, None).unwrap(),
CliCommandInfo { CliCommandInfo {
command: CliCommand::Ping { command: CliCommand::Ping {
lamports: 1, lamports: 1,
@ -1122,7 +1147,7 @@ mod tests {
timeout: Duration::from_secs(3), timeout: Duration::from_secs(3),
commitment_config: CommitmentConfig::default(), commitment_config: CommitmentConfig::default(),
}, },
require_keypair: true signers: vec![default_keypair.into()],
} }
); );
} }

View File

@ -4,19 +4,15 @@ use console::style;
use solana_clap_utils::{ use solana_clap_utils::{
input_parsers::derivation_of, input_parsers::derivation_of,
input_validators::{is_derivation, is_url}, input_validators::{is_derivation, is_url},
keypair::{ keypair::SKIP_SEED_PHRASE_VALIDATION_ARG,
self, keypair_input, KeypairWithSource, ASK_SEED_PHRASE_ARG,
SKIP_SEED_PHRASE_VALIDATION_ARG,
},
}; };
use solana_cli::{ use solana_cli::{
cli::{app, parse_command, process_command, CliCommandInfo, CliConfig, CliError}, cli::{app, parse_command, process_command, CliCommandInfo, CliConfig, CliSigners},
display::{println_name_value, println_name_value_or}, display::{println_name_value, println_name_value_or},
}; };
use solana_cli_config::config::{Config, CONFIG_FILE}; use solana_cli_config::config::{Config, CONFIG_FILE};
use solana_sdk::signature::read_keypair_file; use solana_remote_wallet::remote_wallet::{maybe_wallet_manager, RemoteWalletManager};
use std::{error, sync::Arc};
use std::error;
fn parse_settings(matches: &ArgMatches<'_>) -> Result<bool, Box<dyn error::Error>> { fn parse_settings(matches: &ArgMatches<'_>) -> Result<bool, Box<dyn error::Error>> {
let parse_args = match matches.subcommand() { let parse_args = match matches.subcommand() {
@ -84,7 +80,10 @@ fn parse_settings(matches: &ArgMatches<'_>) -> Result<bool, Box<dyn error::Error
Ok(parse_args) Ok(parse_args)
} }
pub fn parse_args(matches: &ArgMatches<'_>) -> Result<CliConfig, Box<dyn error::Error>> { pub fn parse_args<'a>(
matches: &ArgMatches<'_>,
wallet_manager: Option<Arc<RemoteWalletManager>>,
) -> Result<(CliConfig<'a>, CliSigners), Box<dyn error::Error>> {
let config = if let Some(config_file) = matches.value_of("config_file") { let config = if let Some(config_file) = matches.value_of("config_file") {
Config::load(config_file).unwrap_or_default() Config::load(config_file).unwrap_or_default()
} else { } else {
@ -99,62 +98,29 @@ pub fn parse_args(matches: &ArgMatches<'_>) -> Result<CliConfig, Box<dyn error::
default.json_rpc_url default.json_rpc_url
}; };
let CliCommandInfo { let default_signer_path = if matches.is_present("keypair") {
command, matches.value_of("keypair").unwrap().to_string()
require_keypair, } else if config.keypair_path != "" {
} = parse_command(&matches)?;
let (keypair, keypair_path) = if require_keypair {
let KeypairWithSource { keypair, source } = keypair_input(&matches, "keypair")?;
match source {
keypair::Source::Path => (
keypair,
Some(matches.value_of("keypair").unwrap().to_string()),
),
keypair::Source::SeedPhrase => (keypair, None),
keypair::Source::Generated => {
let keypair_path = if config.keypair_path != "" {
config.keypair_path config.keypair_path
} else { } else {
let default_keypair_path = CliConfig::default_keypair_path(); CliConfig::default_keypair_path()
if !std::path::Path::new(&default_keypair_path).exists() {
return Err(CliError::KeypairFileNotFound(format!(
"Generate a new keypair at {} with `solana-keygen new`",
default_keypair_path
))
.into());
}
default_keypair_path
}; };
let keypair = if keypair_path.starts_with("usb://") { let CliCommandInfo { command, signers } =
keypair parse_command(&matches, &default_signer_path, wallet_manager.as_ref())?;
} else {
read_keypair_file(&keypair_path).or_else(|err| {
Err(CliError::BadParameter(format!(
"{}: Unable to open keypair file: {}",
err, keypair_path
)))
})?
};
(keypair, Some(keypair_path)) Ok((
} CliConfig {
}
} else {
let default = CliConfig::default();
(default.keypair, None)
};
Ok(CliConfig {
command, command,
json_rpc_url, json_rpc_url,
keypair, signers: vec![],
keypair_path, keypair_path: default_signer_path,
derivation_path: derivation_of(matches, "derivation_path"), derivation_path: derivation_of(matches, "derivation_path"),
rpc_client: None, rpc_client: None,
verbose: matches.is_present("verbose"), verbose: matches.is_present("verbose"),
}) },
signers,
))
} }
fn main() -> Result<(), Box<dyn error::Error>> { fn main() -> Result<(), Box<dyn error::Error>> {
@ -201,6 +167,7 @@ fn main() -> Result<(), Box<dyn error::Error>> {
Arg::with_name("derivation_path") Arg::with_name("derivation_path")
.long("derivation-path") .long("derivation-path")
.value_name("ACCOUNT or ACCOUNT/CHANGE") .value_name("ACCOUNT or ACCOUNT/CHANGE")
.global(true)
.takes_value(true) .takes_value(true)
.validator(is_derivation) .validator(is_derivation)
.help("Derivation path to use: m/44'/501'/ACCOUNT'/CHANGE'; default key is device base pubkey: m/44'/501'/0'") .help("Derivation path to use: m/44'/501'/ACCOUNT'/CHANGE'; default key is device base pubkey: m/44'/501'/0'")
@ -212,15 +179,6 @@ fn main() -> Result<(), Box<dyn error::Error>> {
.global(true) .global(true)
.help("Show extra information header"), .help("Show extra information header"),
) )
.arg(
Arg::with_name(ASK_SEED_PHRASE_ARG.name)
.long(ASK_SEED_PHRASE_ARG.long)
.value_name("KEYPAIR NAME")
.global(true)
.takes_value(true)
.possible_values(&["keypair"])
.help(ASK_SEED_PHRASE_ARG.help),
)
.arg( .arg(
Arg::with_name(SKIP_SEED_PHRASE_VALIDATION_ARG.name) Arg::with_name(SKIP_SEED_PHRASE_VALIDATION_ARG.name)
.long(SKIP_SEED_PHRASE_VALIDATION_ARG.long) .long(SKIP_SEED_PHRASE_VALIDATION_ARG.long)
@ -258,7 +216,10 @@ fn main() -> Result<(), Box<dyn error::Error>> {
.get_matches(); .get_matches();
if parse_settings(&matches)? { if parse_settings(&matches)? {
let config = parse_args(&matches)?; let wallet_manager = maybe_wallet_manager()?;
let (mut config, signers) = parse_args(&matches, wallet_manager)?;
config.signers = signers.iter().map(|s| s.as_ref()).collect();
let result = process_command(&config)?; let result = process_command(&config)?;
println!("{}", result); println!("{}", result);
} }

View File

@ -1,19 +1,21 @@
use crate::cli::{ use crate::cli::{
build_balance_message, check_account_for_fee, check_unique_pubkeys, build_balance_message, check_account_for_fee, check_unique_pubkeys, generate_unique_signers,
log_instruction_custom_error, CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult, log_instruction_custom_error, CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult,
SigningAuthority, SignerIndex,
}; };
use crate::offline::BLOCKHASH_ARG;
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use solana_clap_utils::{input_parsers::*, input_validators::*, ArgConstant}; use solana_clap_utils::{
input_parsers::*, input_validators::*, offline::BLOCKHASH_ARG, ArgConstant,
};
use solana_client::rpc_client::RpcClient; use solana_client::rpc_client::RpcClient;
use solana_remote_wallet::remote_wallet::RemoteWalletManager;
use solana_sdk::{ use solana_sdk::{
account::Account, account::Account,
account_utils::StateMut, account_utils::StateMut,
hash::Hash, hash::Hash,
message::Message,
nonce_state::{Meta, NonceState}, nonce_state::{Meta, NonceState},
pubkey::Pubkey, pubkey::Pubkey,
signature::{Keypair, Signer},
system_instruction::{ system_instruction::{
advance_nonce_account, authorize_nonce_account, create_address_with_seed, advance_nonce_account, authorize_nonce_account, create_address_with_seed,
create_nonce_account, create_nonce_account_with_seed, withdraw_nonce_account, NonceError, create_nonce_account, create_nonce_account_with_seed, withdraw_nonce_account, NonceError,
@ -22,6 +24,7 @@ use solana_sdk::{
system_program, system_program,
transaction::Transaction, transaction::Transaction,
}; };
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum CliNonceError { pub enum CliNonceError {
@ -65,8 +68,8 @@ pub fn nonce_authority_arg<'a, 'b>() -> Arg<'a, 'b> {
Arg::with_name(NONCE_AUTHORITY_ARG.name) Arg::with_name(NONCE_AUTHORITY_ARG.name)
.long(NONCE_AUTHORITY_ARG.long) .long(NONCE_AUTHORITY_ARG.long)
.takes_value(true) .takes_value(true)
.value_name("KEYPAIR or PUBKEY") .value_name("KEYPAIR or PUBKEY or REMOTE WALLET PATH")
.validator(is_pubkey_or_keypair_or_ask_keyword) .validator(is_valid_signer)
.help(NONCE_AUTHORITY_ARG.help) .help(NONCE_AUTHORITY_ARG.help)
} }
@ -215,36 +218,61 @@ impl NonceSubCommands for App<'_, '_> {
} }
} }
pub fn parse_authorize_nonce_account(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliError> { pub fn parse_authorize_nonce_account(
matches: &ArgMatches<'_>,
default_signer_path: &str,
wallet_manager: Option<&Arc<RemoteWalletManager>>,
) -> Result<CliCommandInfo, CliError> {
let nonce_account = pubkey_of(matches, "nonce_account_keypair").unwrap(); let nonce_account = pubkey_of(matches, "nonce_account_keypair").unwrap();
let new_authority = pubkey_of(matches, "new_authority").unwrap(); let new_authority = pubkey_of(matches, "new_authority").unwrap();
let nonce_authority = let (nonce_authority, nonce_authority_pubkey) =
SigningAuthority::new_from_matches(&matches, NONCE_AUTHORITY_ARG.name, None)?; signer_of(matches, NONCE_AUTHORITY_ARG.name, wallet_manager)?;
let payer_provided = None;
let signer_info = generate_unique_signers(
vec![payer_provided, nonce_authority],
matches,
default_signer_path,
wallet_manager,
)?;
Ok(CliCommandInfo { Ok(CliCommandInfo {
command: CliCommand::AuthorizeNonceAccount { command: CliCommand::AuthorizeNonceAccount {
nonce_account, nonce_account,
nonce_authority, nonce_authority: signer_info.index_of(nonce_authority_pubkey).unwrap(),
new_authority, new_authority,
}, },
require_keypair: true, signers: signer_info.signers,
}) })
} }
pub fn parse_nonce_create_account(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliError> { pub fn parse_nonce_create_account(
let nonce_account = keypair_of(matches, "nonce_account_keypair").unwrap(); matches: &ArgMatches<'_>,
default_signer_path: &str,
wallet_manager: Option<&Arc<RemoteWalletManager>>,
) -> Result<CliCommandInfo, CliError> {
let (nonce_account, nonce_account_pubkey) =
signer_of(matches, "nonce_account_keypair", wallet_manager)?;
let seed = matches.value_of("seed").map(|s| s.to_string()); let seed = matches.value_of("seed").map(|s| s.to_string());
let lamports = lamports_of_sol(matches, "amount").unwrap(); let lamports = lamports_of_sol(matches, "amount").unwrap();
let nonce_authority = pubkey_of(matches, NONCE_AUTHORITY_ARG.name); let nonce_authority = pubkey_of(matches, NONCE_AUTHORITY_ARG.name);
let payer_provided = None;
let signer_info = generate_unique_signers(
vec![payer_provided, nonce_account],
matches,
default_signer_path,
wallet_manager,
)?;
Ok(CliCommandInfo { Ok(CliCommandInfo {
command: CliCommand::CreateNonceAccount { command: CliCommand::CreateNonceAccount {
nonce_account: nonce_account.into(), nonce_account: signer_info.index_of(nonce_account_pubkey).unwrap(),
seed, seed,
nonce_authority, nonce_authority,
lamports, lamports,
}, },
require_keypair: true, signers: signer_info.signers,
}) })
} }
@ -253,21 +281,33 @@ pub fn parse_get_nonce(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliEr
Ok(CliCommandInfo { Ok(CliCommandInfo {
command: CliCommand::GetNonce(nonce_account_pubkey), command: CliCommand::GetNonce(nonce_account_pubkey),
require_keypair: false, signers: vec![],
}) })
} }
pub fn parse_new_nonce(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliError> { pub fn parse_new_nonce(
matches: &ArgMatches<'_>,
default_signer_path: &str,
wallet_manager: Option<&Arc<RemoteWalletManager>>,
) -> Result<CliCommandInfo, CliError> {
let nonce_account = pubkey_of(matches, "nonce_account_keypair").unwrap(); let nonce_account = pubkey_of(matches, "nonce_account_keypair").unwrap();
let nonce_authority = let (nonce_authority, nonce_authority_pubkey) =
SigningAuthority::new_from_matches(&matches, NONCE_AUTHORITY_ARG.name, None)?; signer_of(matches, NONCE_AUTHORITY_ARG.name, wallet_manager)?;
let payer_provided = None;
let signer_info = generate_unique_signers(
vec![payer_provided, nonce_authority],
matches,
default_signer_path,
wallet_manager,
)?;
Ok(CliCommandInfo { Ok(CliCommandInfo {
command: CliCommand::NewNonce { command: CliCommand::NewNonce {
nonce_account, nonce_account,
nonce_authority, nonce_authority: signer_info.index_of(nonce_authority_pubkey).unwrap(),
}, },
require_keypair: true, signers: signer_info.signers,
}) })
} }
@ -280,27 +320,37 @@ pub fn parse_show_nonce_account(matches: &ArgMatches<'_>) -> Result<CliCommandIn
nonce_account_pubkey, nonce_account_pubkey,
use_lamports_unit, use_lamports_unit,
}, },
require_keypair: false, signers: vec![],
}) })
} }
pub fn parse_withdraw_from_nonce_account( pub fn parse_withdraw_from_nonce_account(
matches: &ArgMatches<'_>, matches: &ArgMatches<'_>,
default_signer_path: &str,
wallet_manager: Option<&Arc<RemoteWalletManager>>,
) -> Result<CliCommandInfo, CliError> { ) -> Result<CliCommandInfo, CliError> {
let nonce_account = pubkey_of(matches, "nonce_account_keypair").unwrap(); let nonce_account = pubkey_of(matches, "nonce_account_keypair").unwrap();
let destination_account_pubkey = pubkey_of(matches, "destination_account_pubkey").unwrap(); let destination_account_pubkey = pubkey_of(matches, "destination_account_pubkey").unwrap();
let lamports = lamports_of_sol(matches, "amount").unwrap(); let lamports = lamports_of_sol(matches, "amount").unwrap();
let nonce_authority = let (nonce_authority, nonce_authority_pubkey) =
SigningAuthority::new_from_matches(&matches, NONCE_AUTHORITY_ARG.name, None)?; signer_of(matches, NONCE_AUTHORITY_ARG.name, wallet_manager)?;
let payer_provided = None;
let signer_info = generate_unique_signers(
vec![payer_provided, nonce_authority],
matches,
default_signer_path,
wallet_manager,
)?;
Ok(CliCommandInfo { Ok(CliCommandInfo {
command: CliCommand::WithdrawFromNonceAccount { command: CliCommand::WithdrawFromNonceAccount {
nonce_account, nonce_account,
nonce_authority, nonce_authority: signer_info.index_of(nonce_authority_pubkey).unwrap(),
destination_account_pubkey, destination_account_pubkey,
lamports, lamports,
}, },
require_keypair: true, signers: signer_info.signers,
}) })
} }
@ -336,41 +386,36 @@ pub fn process_authorize_nonce_account(
rpc_client: &RpcClient, rpc_client: &RpcClient,
config: &CliConfig, config: &CliConfig,
nonce_account: &Pubkey, nonce_account: &Pubkey,
nonce_authority: Option<&SigningAuthority>, nonce_authority: SignerIndex,
new_authority: &Pubkey, new_authority: &Pubkey,
) -> ProcessResult { ) -> ProcessResult {
let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?;
let nonce_authority = nonce_authority let nonce_authority = config.signers[nonce_authority];
.map(|a| a.keypair())
.unwrap_or(&config.keypair);
let ix = authorize_nonce_account(nonce_account, &nonce_authority.pubkey(), new_authority); let ix = authorize_nonce_account(nonce_account, &nonce_authority.pubkey(), new_authority);
let mut tx = Transaction::new_signed_with_payer( let message = Message::new_with_payer(vec![ix], Some(&config.signers[0].pubkey()));
vec![ix], let mut tx = Transaction::new_unsigned(message);
Some(&config.keypair.pubkey()), tx.try_sign(&config.signers, recent_blockhash)?;
&[&config.keypair, nonce_authority],
recent_blockhash,
);
check_account_for_fee( check_account_for_fee(
rpc_client, rpc_client,
&config.keypair.pubkey(), &config.signers[0].pubkey(),
&fee_calculator, &fee_calculator,
&tx.message, &tx.message,
)?; )?;
let result = let result = rpc_client.send_and_confirm_transaction(&mut tx, &config.signers);
rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair, nonce_authority]);
log_instruction_custom_error::<NonceError>(result) log_instruction_custom_error::<NonceError>(result)
} }
pub fn process_create_nonce_account( pub fn process_create_nonce_account(
rpc_client: &RpcClient, rpc_client: &RpcClient,
config: &CliConfig, config: &CliConfig,
nonce_account: &Keypair, nonce_account: SignerIndex,
seed: Option<String>, seed: Option<String>,
nonce_authority: Option<Pubkey>, nonce_authority: Option<Pubkey>,
lamports: u64, lamports: u64,
) -> ProcessResult { ) -> ProcessResult {
let nonce_account_pubkey = nonce_account.pubkey(); let nonce_account_pubkey = config.signers[nonce_account].pubkey();
let nonce_account_address = if let Some(seed) = seed.clone() { let nonce_account_address = if let Some(seed) = seed.clone() {
create_address_with_seed(&nonce_account_pubkey, &seed, &system_program::id())? create_address_with_seed(&nonce_account_pubkey, &seed, &system_program::id())?
} else { } else {
@ -378,7 +423,7 @@ pub fn process_create_nonce_account(
}; };
check_unique_pubkeys( check_unique_pubkeys(
(&config.keypair.pubkey(), "cli keypair".to_string()), (&config.signers[0].pubkey(), "cli keypair".to_string()),
(&nonce_account_address, "nonce_account".to_string()), (&nonce_account_address, "nonce_account".to_string()),
)?; )?;
@ -405,11 +450,11 @@ pub fn process_create_nonce_account(
.into()); .into());
} }
let nonce_authority = nonce_authority.unwrap_or_else(|| config.keypair.pubkey()); let nonce_authority = nonce_authority.unwrap_or_else(|| config.signers[0].pubkey());
let ixs = if let Some(seed) = seed { let ixs = if let Some(seed) = seed {
create_nonce_account_with_seed( create_nonce_account_with_seed(
&config.keypair.pubkey(), // from &config.signers[0].pubkey(), // from
&nonce_account_address, // to &nonce_account_address, // to
&nonce_account_pubkey, // base &nonce_account_pubkey, // base
&seed, // seed &seed, // seed
@ -418,7 +463,7 @@ pub fn process_create_nonce_account(
) )
} else { } else {
create_nonce_account( create_nonce_account(
&config.keypair.pubkey(), &config.signers[0].pubkey(),
&nonce_account_pubkey, &nonce_account_pubkey,
&nonce_authority, &nonce_authority,
lamports, lamports,
@ -427,25 +472,17 @@ pub fn process_create_nonce_account(
let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?;
let signers = if nonce_account_pubkey != config.keypair.pubkey() { let message = Message::new_with_payer(ixs, Some(&config.signers[0].pubkey()));
vec![&config.keypair, nonce_account] // both must sign if `from` and `to` differ let mut tx = Transaction::new_unsigned(message);
} else { tx.try_sign(&config.signers, recent_blockhash)?;
vec![&config.keypair] // when stake_account == config.keypair and there's a seed, we only need one signature
};
let mut tx = Transaction::new_signed_with_payer(
ixs,
Some(&config.keypair.pubkey()),
&signers,
recent_blockhash,
);
check_account_for_fee( check_account_for_fee(
rpc_client, rpc_client,
&config.keypair.pubkey(), &config.signers[0].pubkey(),
&fee_calculator, &fee_calculator,
&tx.message, &tx.message,
)?; )?;
let result = rpc_client.send_and_confirm_transaction(&mut tx, &signers); let result = rpc_client.send_and_confirm_transaction(&mut tx, &config.signers);
log_instruction_custom_error::<SystemError>(result) log_instruction_custom_error::<SystemError>(result)
} }
@ -473,10 +510,10 @@ pub fn process_new_nonce(
rpc_client: &RpcClient, rpc_client: &RpcClient,
config: &CliConfig, config: &CliConfig,
nonce_account: &Pubkey, nonce_account: &Pubkey,
nonce_authority: Option<&SigningAuthority>, nonce_authority: SignerIndex,
) -> ProcessResult { ) -> ProcessResult {
check_unique_pubkeys( check_unique_pubkeys(
(&config.keypair.pubkey(), "cli keypair".to_string()), (&config.signers[0].pubkey(), "cli keypair".to_string()),
(&nonce_account, "nonce_account_pubkey".to_string()), (&nonce_account, "nonce_account_pubkey".to_string()),
)?; )?;
@ -487,25 +524,20 @@ pub fn process_new_nonce(
.into()); .into());
} }
let nonce_authority = nonce_authority let nonce_authority = config.signers[nonce_authority];
.map(|a| a.keypair())
.unwrap_or(&config.keypair);
let ix = advance_nonce_account(&nonce_account, &nonce_authority.pubkey()); let ix = advance_nonce_account(&nonce_account, &nonce_authority.pubkey());
let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?;
let mut tx = Transaction::new_signed_with_payer( let message = Message::new_with_payer(vec![ix], Some(&config.signers[0].pubkey()));
vec![ix], let mut tx = Transaction::new_unsigned(message);
Some(&config.keypair.pubkey()), tx.try_sign(&config.signers, recent_blockhash)?;
&[&config.keypair, nonce_authority],
recent_blockhash,
);
check_account_for_fee( check_account_for_fee(
rpc_client, rpc_client,
&config.keypair.pubkey(), &config.signers[0].pubkey(),
&fee_calculator, &fee_calculator,
&tx.message, &tx.message,
)?; )?;
let result = let result =
rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair, nonce_authority]); rpc_client.send_and_confirm_transaction(&mut tx, &[config.signers[0], nonce_authority]);
log_instruction_custom_error::<SystemError>(result) log_instruction_custom_error::<SystemError>(result)
} }
@ -562,35 +594,29 @@ pub fn process_withdraw_from_nonce_account(
rpc_client: &RpcClient, rpc_client: &RpcClient,
config: &CliConfig, config: &CliConfig,
nonce_account: &Pubkey, nonce_account: &Pubkey,
nonce_authority: Option<&SigningAuthority>, nonce_authority: SignerIndex,
destination_account_pubkey: &Pubkey, destination_account_pubkey: &Pubkey,
lamports: u64, lamports: u64,
) -> ProcessResult { ) -> ProcessResult {
let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?;
let nonce_authority = nonce_authority let nonce_authority = config.signers[nonce_authority];
.map(|a| a.keypair())
.unwrap_or(&config.keypair);
let ix = withdraw_nonce_account( let ix = withdraw_nonce_account(
nonce_account, nonce_account,
&nonce_authority.pubkey(), &nonce_authority.pubkey(),
destination_account_pubkey, destination_account_pubkey,
lamports, lamports,
); );
let mut tx = Transaction::new_signed_with_payer( let message = Message::new_with_payer(vec![ix], Some(&config.signers[0].pubkey()));
vec![ix], let mut tx = Transaction::new_unsigned(message);
Some(&config.keypair.pubkey()), tx.try_sign(&config.signers, recent_blockhash)?;
&[&config.keypair, nonce_authority],
recent_blockhash,
);
check_account_for_fee( check_account_for_fee(
rpc_client, rpc_client,
&config.keypair.pubkey(), &config.signers[0].pubkey(),
&fee_calculator, &fee_calculator,
&tx.message, &tx.message,
)?; )?;
let result = let result = rpc_client.send_and_confirm_transaction(&mut tx, &config.signers);
rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair, nonce_authority]);
log_instruction_custom_error::<NonceError>(result) log_instruction_custom_error::<NonceError>(result)
} }
@ -602,7 +628,7 @@ mod tests {
account::Account, account::Account,
hash::hash, hash::hash,
nonce_state::{Meta as NonceMeta, NonceState}, nonce_state::{Meta as NonceMeta, NonceState},
signature::{read_keypair_file, write_keypair}, signature::{read_keypair_file, write_keypair, Keypair, Signer},
system_program, system_program,
}; };
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
@ -615,6 +641,9 @@ mod tests {
#[test] #[test]
fn test_parse_command() { fn test_parse_command() {
let test_commands = app("test", "desc", "version"); let test_commands = app("test", "desc", "version");
let default_keypair = Keypair::new();
let (default_keypair_file, mut tmp_file) = make_tmp_file();
write_keypair(&default_keypair, tmp_file.as_file_mut()).unwrap();
let (keypair_file, mut tmp_file) = make_tmp_file(); let (keypair_file, mut tmp_file) = make_tmp_file();
let nonce_account_keypair = Keypair::new(); let nonce_account_keypair = Keypair::new();
write_keypair(&nonce_account_keypair, tmp_file.as_file_mut()).unwrap(); write_keypair(&nonce_account_keypair, tmp_file.as_file_mut()).unwrap();
@ -633,14 +662,14 @@ mod tests {
&Pubkey::default().to_string(), &Pubkey::default().to_string(),
]); ]);
assert_eq!( assert_eq!(
parse_command(&test_authorize_nonce_account).unwrap(), parse_command(&test_authorize_nonce_account, &default_keypair_file, None).unwrap(),
CliCommandInfo { CliCommandInfo {
command: CliCommand::AuthorizeNonceAccount { command: CliCommand::AuthorizeNonceAccount {
nonce_account: nonce_account_pubkey, nonce_account: nonce_account_pubkey,
nonce_authority: None, nonce_authority: 0,
new_authority: Pubkey::default(), new_authority: Pubkey::default(),
}, },
require_keypair: true, signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()],
} }
); );
@ -654,16 +683,17 @@ mod tests {
&authority_keypair_file, &authority_keypair_file,
]); ]);
assert_eq!( assert_eq!(
parse_command(&test_authorize_nonce_account).unwrap(), parse_command(&test_authorize_nonce_account, &default_keypair_file, None).unwrap(),
CliCommandInfo { CliCommandInfo {
command: CliCommand::AuthorizeNonceAccount { command: CliCommand::AuthorizeNonceAccount {
nonce_account: read_keypair_file(&keypair_file).unwrap().pubkey(), nonce_account: read_keypair_file(&keypair_file).unwrap().pubkey(),
nonce_authority: Some( nonce_authority: 1,
read_keypair_file(&authority_keypair_file).unwrap().into()
),
new_authority: Pubkey::default(), new_authority: Pubkey::default(),
}, },
require_keypair: true, signers: vec![
read_keypair_file(&default_keypair_file).unwrap().into(),
read_keypair_file(&authority_keypair_file).unwrap().into()
],
} }
); );
@ -675,15 +705,18 @@ mod tests {
"50", "50",
]); ]);
assert_eq!( assert_eq!(
parse_command(&test_create_nonce_account).unwrap(), parse_command(&test_create_nonce_account, &default_keypair_file, None).unwrap(),
CliCommandInfo { CliCommandInfo {
command: CliCommand::CreateNonceAccount { command: CliCommand::CreateNonceAccount {
nonce_account: read_keypair_file(&keypair_file).unwrap().into(), nonce_account: 1,
seed: None, seed: None,
nonce_authority: None, nonce_authority: None,
lamports: 50_000_000_000, lamports: 50_000_000_000,
}, },
require_keypair: true signers: vec![
read_keypair_file(&default_keypair_file).unwrap().into(),
read_keypair_file(&keypair_file).unwrap().into()
],
} }
); );
@ -697,17 +730,18 @@ mod tests {
&authority_keypair_file, &authority_keypair_file,
]); ]);
assert_eq!( assert_eq!(
parse_command(&test_create_nonce_account).unwrap(), parse_command(&test_create_nonce_account, &default_keypair_file, None).unwrap(),
CliCommandInfo { CliCommandInfo {
command: CliCommand::CreateNonceAccount { command: CliCommand::CreateNonceAccount {
nonce_account: read_keypair_file(&keypair_file).unwrap().into(), nonce_account: 1,
seed: None, seed: None,
nonce_authority: Some( nonce_authority: Some(nonce_authority_keypair.pubkey()),
read_keypair_file(&authority_keypair_file).unwrap().pubkey()
),
lamports: 50_000_000_000, lamports: 50_000_000_000,
}, },
require_keypair: true signers: vec![
read_keypair_file(&default_keypair_file).unwrap().into(),
read_keypair_file(&keypair_file).unwrap().into()
],
} }
); );
@ -718,10 +752,10 @@ mod tests {
&nonce_account_string, &nonce_account_string,
]); ]);
assert_eq!( assert_eq!(
parse_command(&test_get_nonce).unwrap(), parse_command(&test_get_nonce, &default_keypair_file, None).unwrap(),
CliCommandInfo { CliCommandInfo {
command: CliCommand::GetNonce(nonce_account_keypair.pubkey(),), command: CliCommand::GetNonce(nonce_account_keypair.pubkey()),
require_keypair: false signers: vec![],
} }
); );
@ -732,13 +766,13 @@ mod tests {
.get_matches_from(vec!["test", "new-nonce", &keypair_file]); .get_matches_from(vec!["test", "new-nonce", &keypair_file]);
let nonce_account = read_keypair_file(&keypair_file).unwrap(); let nonce_account = read_keypair_file(&keypair_file).unwrap();
assert_eq!( assert_eq!(
parse_command(&test_new_nonce).unwrap(), parse_command(&test_new_nonce, &default_keypair_file, None).unwrap(),
CliCommandInfo { CliCommandInfo {
command: CliCommand::NewNonce { command: CliCommand::NewNonce {
nonce_account: nonce_account.pubkey(), nonce_account: nonce_account.pubkey(),
nonce_authority: None, nonce_authority: 0,
}, },
require_keypair: true signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()],
} }
); );
@ -752,15 +786,16 @@ mod tests {
]); ]);
let nonce_account = read_keypair_file(&keypair_file).unwrap(); let nonce_account = read_keypair_file(&keypair_file).unwrap();
assert_eq!( assert_eq!(
parse_command(&test_new_nonce).unwrap(), parse_command(&test_new_nonce, &default_keypair_file, None).unwrap(),
CliCommandInfo { CliCommandInfo {
command: CliCommand::NewNonce { command: CliCommand::NewNonce {
nonce_account: nonce_account.pubkey(), nonce_account: nonce_account.pubkey(),
nonce_authority: Some( nonce_authority: 1,
read_keypair_file(&authority_keypair_file).unwrap().into()
),
}, },
require_keypair: true signers: vec![
read_keypair_file(&default_keypair_file).unwrap().into(),
read_keypair_file(&authority_keypair_file).unwrap().into()
],
} }
); );
@ -771,13 +806,13 @@ mod tests {
&nonce_account_string, &nonce_account_string,
]); ]);
assert_eq!( assert_eq!(
parse_command(&test_show_nonce_account).unwrap(), parse_command(&test_show_nonce_account, &default_keypair_file, None).unwrap(),
CliCommandInfo { CliCommandInfo {
command: CliCommand::ShowNonceAccount { command: CliCommand::ShowNonceAccount {
nonce_account_pubkey: nonce_account_keypair.pubkey(), nonce_account_pubkey: nonce_account_keypair.pubkey(),
use_lamports_unit: false, use_lamports_unit: false,
}, },
require_keypair: false signers: vec![],
} }
); );
@ -790,15 +825,20 @@ mod tests {
"42", "42",
]); ]);
assert_eq!( assert_eq!(
parse_command(&test_withdraw_from_nonce_account).unwrap(), parse_command(
&test_withdraw_from_nonce_account,
&default_keypair_file,
None
)
.unwrap(),
CliCommandInfo { CliCommandInfo {
command: CliCommand::WithdrawFromNonceAccount { command: CliCommand::WithdrawFromNonceAccount {
nonce_account: read_keypair_file(&keypair_file).unwrap().pubkey(), nonce_account: read_keypair_file(&keypair_file).unwrap().pubkey(),
nonce_authority: None, nonce_authority: 0,
destination_account_pubkey: nonce_account_pubkey, destination_account_pubkey: nonce_account_pubkey,
lamports: 42_000_000_000 lamports: 42_000_000_000
}, },
require_keypair: true signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()],
} }
); );
@ -810,15 +850,20 @@ mod tests {
"42", "42",
]); ]);
assert_eq!( assert_eq!(
parse_command(&test_withdraw_from_nonce_account).unwrap(), parse_command(
&test_withdraw_from_nonce_account,
&default_keypair_file,
None
)
.unwrap(),
CliCommandInfo { CliCommandInfo {
command: CliCommand::WithdrawFromNonceAccount { command: CliCommand::WithdrawFromNonceAccount {
nonce_account: read_keypair_file(&keypair_file).unwrap().pubkey(), nonce_account: read_keypair_file(&keypair_file).unwrap().pubkey(),
nonce_authority: None, nonce_authority: 0,
destination_account_pubkey: nonce_account_pubkey, destination_account_pubkey: nonce_account_pubkey,
lamports: 42000000000 lamports: 42000000000
}, },
require_keypair: true signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()],
} }
); );
@ -833,17 +878,23 @@ mod tests {
&authority_keypair_file, &authority_keypair_file,
]); ]);
assert_eq!( assert_eq!(
parse_command(&test_withdraw_from_nonce_account).unwrap(), parse_command(
&test_withdraw_from_nonce_account,
&default_keypair_file,
None
)
.unwrap(),
CliCommandInfo { CliCommandInfo {
command: CliCommand::WithdrawFromNonceAccount { command: CliCommand::WithdrawFromNonceAccount {
nonce_account: read_keypair_file(&keypair_file).unwrap().pubkey(), nonce_account: read_keypair_file(&keypair_file).unwrap().pubkey(),
nonce_authority: Some( nonce_authority: 1,
read_keypair_file(&authority_keypair_file).unwrap().into()
),
destination_account_pubkey: nonce_account_pubkey, destination_account_pubkey: nonce_account_pubkey,
lamports: 42_000_000_000 lamports: 42_000_000_000
}, },
require_keypair: true signers: vec![
read_keypair_file(&default_keypair_file).unwrap().into(),
read_keypair_file(&authority_keypair_file).unwrap().into()
],
} }
); );
} }

View File

@ -3,30 +3,12 @@ use serde_json::Value;
use solana_clap_utils::{ use solana_clap_utils::{
input_parsers::value_of, input_parsers::value_of,
input_validators::{is_hash, is_pubkey_sig}, input_validators::{is_hash, is_pubkey_sig},
ArgConstant, offline::{BLOCKHASH_ARG, SIGNER_ARG, SIGN_ONLY_ARG},
}; };
use solana_client::rpc_client::RpcClient; use solana_client::rpc_client::RpcClient;
use solana_sdk::{fee_calculator::FeeCalculator, hash::Hash, pubkey::Pubkey, signature::Signature}; use solana_sdk::{fee_calculator::FeeCalculator, hash::Hash, pubkey::Pubkey, signature::Signature};
use std::str::FromStr; use std::str::FromStr;
pub const BLOCKHASH_ARG: ArgConstant<'static> = ArgConstant {
name: "blockhash",
long: "blockhash",
help: "Use the supplied blockhash",
};
pub const SIGN_ONLY_ARG: ArgConstant<'static> = ArgConstant {
name: "sign_only",
long: "sign-only",
help: "Sign the transaction offline",
};
pub const SIGNER_ARG: ArgConstant<'static> = ArgConstant {
name: "signer",
long: "signer",
help: "Provid a public-key/signature pair for the transaction",
};
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum BlockhashQuery { pub enum BlockhashQuery {
None(Hash, FeeCalculator), None(Hash, FeeCalculator),

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,17 @@
use crate::cli::{ use crate::cli::{
check_account_for_fee, check_unique_pubkeys, log_instruction_custom_error, CliCommand, check_account_for_fee, check_unique_pubkeys, log_instruction_custom_error, CliCommand,
CliCommandInfo, CliConfig, CliError, ProcessResult, CliCommandInfo, CliConfig, CliError, ProcessResult, SignerIndex,
}; };
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use solana_clap_utils::{input_parsers::*, input_validators::*}; use solana_clap_utils::{input_parsers::*, input_validators::*, keypair::signer_from_path};
use solana_client::rpc_client::RpcClient; use solana_client::rpc_client::RpcClient;
use solana_sdk::signature::Keypair; use solana_remote_wallet::remote_wallet::RemoteWalletManager;
use solana_sdk::{ use solana_sdk::{
account_utils::StateMut, message::Message, pubkey::Pubkey, signature::Signer, account_utils::StateMut, message::Message, pubkey::Pubkey, system_instruction::SystemError,
system_instruction::SystemError, transaction::Transaction, transaction::Transaction,
}; };
use solana_storage_program::storage_instruction::{self, StorageAccountType}; use solana_storage_program::storage_instruction::{self, StorageAccountType};
use std::sync::Arc;
pub trait StorageSubCommands { pub trait StorageSubCommands {
fn storage_subcommands(self) -> Self; fn storage_subcommands(self) -> Self;
@ -99,35 +100,49 @@ impl StorageSubCommands for App<'_, '_> {
pub fn parse_storage_create_archiver_account( pub fn parse_storage_create_archiver_account(
matches: &ArgMatches<'_>, matches: &ArgMatches<'_>,
default_signer_path: &str,
wallet_manager: Option<&Arc<RemoteWalletManager>>,
) -> Result<CliCommandInfo, CliError> { ) -> Result<CliCommandInfo, CliError> {
let account_owner = pubkey_of(matches, "storage_account_owner").unwrap(); let account_owner = pubkey_of(matches, "storage_account_owner").unwrap();
let storage_account = keypair_of(matches, "storage_account").unwrap(); let storage_account = keypair_of(matches, "storage_account").unwrap();
Ok(CliCommandInfo { Ok(CliCommandInfo {
command: CliCommand::CreateStorageAccount { command: CliCommand::CreateStorageAccount {
account_owner, account_owner,
storage_account: storage_account.into(), storage_account: 1,
account_type: StorageAccountType::Archiver, account_type: StorageAccountType::Archiver,
}, },
require_keypair: true, signers: vec![
signer_from_path(matches, default_signer_path, "keypair", wallet_manager)?,
storage_account.into(),
],
}) })
} }
pub fn parse_storage_create_validator_account( pub fn parse_storage_create_validator_account(
matches: &ArgMatches<'_>, matches: &ArgMatches<'_>,
default_signer_path: &str,
wallet_manager: Option<&Arc<RemoteWalletManager>>,
) -> Result<CliCommandInfo, CliError> { ) -> Result<CliCommandInfo, CliError> {
let account_owner = pubkey_of(matches, "storage_account_owner").unwrap(); let account_owner = pubkey_of(matches, "storage_account_owner").unwrap();
let storage_account = keypair_of(matches, "storage_account").unwrap(); let storage_account = keypair_of(matches, "storage_account").unwrap();
Ok(CliCommandInfo { Ok(CliCommandInfo {
command: CliCommand::CreateStorageAccount { command: CliCommand::CreateStorageAccount {
account_owner, account_owner,
storage_account: storage_account.into(), storage_account: 1,
account_type: StorageAccountType::Validator, account_type: StorageAccountType::Validator,
}, },
require_keypair: true, signers: vec![
signer_from_path(matches, default_signer_path, "keypair", wallet_manager)?,
storage_account.into(),
],
}) })
} }
pub fn parse_storage_claim_reward(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliError> { pub fn parse_storage_claim_reward(
matches: &ArgMatches<'_>,
default_signer_path: &str,
wallet_manager: Option<&Arc<RemoteWalletManager>>,
) -> Result<CliCommandInfo, CliError> {
let node_account_pubkey = pubkey_of(matches, "node_account_pubkey").unwrap(); let node_account_pubkey = pubkey_of(matches, "node_account_pubkey").unwrap();
let storage_account_pubkey = pubkey_of(matches, "storage_account_pubkey").unwrap(); let storage_account_pubkey = pubkey_of(matches, "storage_account_pubkey").unwrap();
Ok(CliCommandInfo { Ok(CliCommandInfo {
@ -135,7 +150,12 @@ pub fn parse_storage_claim_reward(matches: &ArgMatches<'_>) -> Result<CliCommand
node_account_pubkey, node_account_pubkey,
storage_account_pubkey, storage_account_pubkey,
}, },
require_keypair: true, signers: vec![signer_from_path(
matches,
default_signer_path,
"keypair",
wallet_manager,
)?],
}) })
} }
@ -145,20 +165,21 @@ pub fn parse_storage_get_account_command(
let storage_account_pubkey = pubkey_of(matches, "storage_account_pubkey").unwrap(); let storage_account_pubkey = pubkey_of(matches, "storage_account_pubkey").unwrap();
Ok(CliCommandInfo { Ok(CliCommandInfo {
command: CliCommand::ShowStorageAccount(storage_account_pubkey), command: CliCommand::ShowStorageAccount(storage_account_pubkey),
require_keypair: false, signers: vec![],
}) })
} }
pub fn process_create_storage_account( pub fn process_create_storage_account(
rpc_client: &RpcClient, rpc_client: &RpcClient,
config: &CliConfig, config: &CliConfig,
storage_account: SignerIndex,
account_owner: &Pubkey, account_owner: &Pubkey,
storage_account: &Keypair,
account_type: StorageAccountType, account_type: StorageAccountType,
) -> ProcessResult { ) -> ProcessResult {
let storage_account = config.signers[storage_account];
let storage_account_pubkey = storage_account.pubkey(); let storage_account_pubkey = storage_account.pubkey();
check_unique_pubkeys( check_unique_pubkeys(
(&config.keypair.pubkey(), "cli keypair".to_string()), (&config.signers[0].pubkey(), "cli keypair".to_string()),
( (
&storage_account_pubkey, &storage_account_pubkey,
"storage_account_pubkey".to_string(), "storage_account_pubkey".to_string(),
@ -183,7 +204,7 @@ pub fn process_create_storage_account(
.max(1); .max(1);
let ixs = storage_instruction::create_storage_account( let ixs = storage_instruction::create_storage_account(
&config.keypair.pubkey(), &config.signers[0].pubkey(),
&account_owner, &account_owner,
&storage_account_pubkey, &storage_account_pubkey,
required_balance, required_balance,
@ -191,19 +212,16 @@ pub fn process_create_storage_account(
); );
let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?;
let mut tx = Transaction::new_signed_instructions( let message = Message::new(ixs);
&[&config.keypair, &storage_account], let mut tx = Transaction::new_unsigned(message);
ixs, tx.try_sign(&config.signers, recent_blockhash)?;
recent_blockhash,
);
check_account_for_fee( check_account_for_fee(
rpc_client, rpc_client,
&config.keypair.pubkey(), &config.signers[0].pubkey(),
&fee_calculator, &fee_calculator,
&tx.message, &tx.message,
)?; )?;
let result = let result = rpc_client.send_and_confirm_transaction(&mut tx, &config.signers);
rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair, &storage_account]);
log_instruction_custom_error::<SystemError>(result) log_instruction_custom_error::<SystemError>(result)
} }
@ -217,13 +235,13 @@ pub fn process_claim_storage_reward(
let instruction = let instruction =
storage_instruction::claim_reward(node_account_pubkey, storage_account_pubkey); storage_instruction::claim_reward(node_account_pubkey, storage_account_pubkey);
let signers = [&config.keypair]; let signers = [config.signers[0]];
let message = Message::new_with_payer(vec![instruction], Some(&signers[0].pubkey())); let message = Message::new_with_payer(vec![instruction], Some(&signers[0].pubkey()));
let mut tx = Transaction::new_unsigned(message);
let mut tx = Transaction::new(&signers, message, recent_blockhash); tx.try_sign(&signers, recent_blockhash)?;
check_account_for_fee( check_account_for_fee(
rpc_client, rpc_client,
&config.keypair.pubkey(), &config.signers[0].pubkey(),
&fee_calculator, &fee_calculator,
&tx.message, &tx.message,
)?; )?;
@ -259,7 +277,7 @@ pub fn process_show_storage_account(
mod tests { mod tests {
use super::*; use super::*;
use crate::cli::{app, parse_command}; use crate::cli::{app, parse_command};
use solana_sdk::signature::write_keypair; use solana_sdk::signature::{read_keypair_file, write_keypair, Keypair, Signer};
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
fn make_tmp_file() -> (String, NamedTempFile) { fn make_tmp_file() -> (String, NamedTempFile) {
@ -273,6 +291,10 @@ mod tests {
let pubkey = Pubkey::new_rand(); let pubkey = Pubkey::new_rand();
let pubkey_string = pubkey.to_string(); let pubkey_string = pubkey.to_string();
let default_keypair = Keypair::new();
let (default_keypair_file, mut tmp_file) = make_tmp_file();
write_keypair(&default_keypair, tmp_file.as_file_mut()).unwrap();
let (keypair_file, mut tmp_file) = make_tmp_file(); let (keypair_file, mut tmp_file) = make_tmp_file();
let storage_account_keypair = Keypair::new(); let storage_account_keypair = Keypair::new();
write_keypair(&storage_account_keypair, tmp_file.as_file_mut()).unwrap(); write_keypair(&storage_account_keypair, tmp_file.as_file_mut()).unwrap();
@ -284,14 +306,22 @@ mod tests {
&keypair_file, &keypair_file,
]); ]);
assert_eq!( assert_eq!(
parse_command(&test_create_archiver_storage_account).unwrap(), parse_command(
&test_create_archiver_storage_account,
&default_keypair_file,
None
)
.unwrap(),
CliCommandInfo { CliCommandInfo {
command: CliCommand::CreateStorageAccount { command: CliCommand::CreateStorageAccount {
account_owner: pubkey, account_owner: pubkey,
storage_account: storage_account_keypair.into(), storage_account: 1,
account_type: StorageAccountType::Archiver, account_type: StorageAccountType::Archiver,
}, },
require_keypair: true signers: vec![
read_keypair_file(&default_keypair_file).unwrap().into(),
storage_account_keypair.into()
],
} }
); );
@ -308,14 +338,22 @@ mod tests {
&keypair_file, &keypair_file,
]); ]);
assert_eq!( assert_eq!(
parse_command(&test_create_validator_storage_account).unwrap(), parse_command(
&test_create_validator_storage_account,
&default_keypair_file,
None
)
.unwrap(),
CliCommandInfo { CliCommandInfo {
command: CliCommand::CreateStorageAccount { command: CliCommand::CreateStorageAccount {
account_owner: pubkey, account_owner: pubkey,
storage_account: storage_account_keypair.into(), storage_account: 1,
account_type: StorageAccountType::Validator, account_type: StorageAccountType::Validator,
}, },
require_keypair: true signers: vec![
read_keypair_file(&default_keypair_file).unwrap().into(),
storage_account_keypair.into()
],
} }
); );
@ -326,13 +364,13 @@ mod tests {
&storage_account_string, &storage_account_string,
]); ]);
assert_eq!( assert_eq!(
parse_command(&test_claim_storage_reward).unwrap(), parse_command(&test_claim_storage_reward, &default_keypair_file, None).unwrap(),
CliCommandInfo { CliCommandInfo {
command: CliCommand::ClaimStorageReward { command: CliCommand::ClaimStorageReward {
node_account_pubkey: pubkey, node_account_pubkey: pubkey,
storage_account_pubkey, storage_account_pubkey,
}, },
require_keypair: true signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()],
} }
); );
} }

View File

@ -11,9 +11,11 @@ use serde_json::{Map, Value};
use solana_clap_utils::{ use solana_clap_utils::{
input_parsers::pubkey_of, input_parsers::pubkey_of,
input_validators::{is_pubkey, is_url}, input_validators::{is_pubkey, is_url},
keypair::signer_from_path,
}; };
use solana_client::rpc_client::RpcClient; use solana_client::rpc_client::RpcClient;
use solana_config_program::{config_instruction, get_config_data, ConfigKeys, ConfigState}; use solana_config_program::{config_instruction, get_config_data, ConfigKeys, ConfigState};
use solana_remote_wallet::remote_wallet::RemoteWalletManager;
use solana_sdk::{ use solana_sdk::{
account::Account, account::Account,
commitment_config::CommitmentConfig, commitment_config::CommitmentConfig,
@ -22,7 +24,7 @@ use solana_sdk::{
signature::{Keypair, Signer}, signature::{Keypair, Signer},
transaction::Transaction, transaction::Transaction,
}; };
use std::error; use std::{error, sync::Arc};
use titlecase::titlecase; use titlecase::titlecase;
pub const MAX_SHORT_FIELD_LENGTH: usize = 70; pub const MAX_SHORT_FIELD_LENGTH: usize = 70;
@ -224,17 +226,26 @@ impl ValidatorInfoSubCommands for App<'_, '_> {
} }
} }
pub fn parse_validator_info_command(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliError> { pub fn parse_validator_info_command(
matches: &ArgMatches<'_>,
default_signer_path: &str,
wallet_manager: Option<&Arc<RemoteWalletManager>>,
) -> Result<CliCommandInfo, CliError> {
let info_pubkey = pubkey_of(matches, "info_pubkey"); let info_pubkey = pubkey_of(matches, "info_pubkey");
// Prepare validator info // Prepare validator info
let validator_info = parse_args(&matches); let validator_info = parse_args(matches);
Ok(CliCommandInfo { Ok(CliCommandInfo {
command: CliCommand::SetValidatorInfo { command: CliCommand::SetValidatorInfo {
validator_info, validator_info,
force_keybase: matches.is_present("force"), force_keybase: matches.is_present("force"),
info_pubkey, info_pubkey,
}, },
require_keypair: true, signers: vec![signer_from_path(
matches,
default_signer_path,
"keypair",
wallet_manager,
)?],
}) })
} }
@ -244,7 +255,7 @@ pub fn parse_get_validator_info_command(
let info_pubkey = pubkey_of(matches, "info_pubkey"); let info_pubkey = pubkey_of(matches, "info_pubkey");
Ok(CliCommandInfo { Ok(CliCommandInfo {
command: CliCommand::GetValidatorInfo(info_pubkey), command: CliCommand::GetValidatorInfo(info_pubkey),
require_keypair: false, signers: vec![],
}) })
} }
@ -257,7 +268,7 @@ pub fn process_set_validator_info(
) -> ProcessResult { ) -> ProcessResult {
// Validate keybase username // Validate keybase username
if let Some(string) = validator_info.get("keybaseUsername") { if let Some(string) = validator_info.get("keybaseUsername") {
let result = verify_keybase(&config.keypair.pubkey(), &string); let result = verify_keybase(&config.signers[0].pubkey(), &string);
if result.is_err() { if result.is_err() {
if force_keybase { if force_keybase {
println!("--force supplied, ignoring: {:?}", result); println!("--force supplied, ignoring: {:?}", result);
@ -282,7 +293,7 @@ pub fn process_set_validator_info(
}) })
.find(|(pubkey, account)| { .find(|(pubkey, account)| {
let (validator_pubkey, _) = parse_validator_info(&pubkey, &account).unwrap(); let (validator_pubkey, _) = parse_validator_info(&pubkey, &account).unwrap();
validator_pubkey == config.keypair.pubkey() validator_pubkey == config.signers[0].pubkey()
}); });
// Create validator-info keypair to use if info_pubkey not provided or does not exist // Create validator-info keypair to use if info_pubkey not provided or does not exist
@ -300,8 +311,8 @@ pub fn process_set_validator_info(
.poll_get_balance_with_commitment(&info_pubkey, CommitmentConfig::default()) .poll_get_balance_with_commitment(&info_pubkey, CommitmentConfig::default())
.unwrap_or(0); .unwrap_or(0);
let keys = vec![(id(), false), (config.keypair.pubkey(), true)]; let keys = vec![(id(), false), (config.signers[0].pubkey(), true)];
let (message, signers): (Message, Vec<&Keypair>) = if balance == 0 { let (message, signers): (Message, Vec<&dyn Signer>) = if balance == 0 {
if info_pubkey != info_keypair.pubkey() { if info_pubkey != info_keypair.pubkey() {
println!( println!(
"Account {:?} does not exist. Generating new keypair...", "Account {:?} does not exist. Generating new keypair...",
@ -311,12 +322,12 @@ pub fn process_set_validator_info(
} }
println!( println!(
"Publishing info for Validator {:?}", "Publishing info for Validator {:?}",
config.keypair.pubkey() config.signers[0].pubkey()
); );
let lamports = rpc_client let lamports = rpc_client
.get_minimum_balance_for_rent_exemption(ValidatorInfo::max_space() as usize)?; .get_minimum_balance_for_rent_exemption(ValidatorInfo::max_space() as usize)?;
let mut instructions = config_instruction::create_account::<ValidatorInfo>( let mut instructions = config_instruction::create_account::<ValidatorInfo>(
&config.keypair.pubkey(), &config.signers[0].pubkey(),
&info_keypair.pubkey(), &info_keypair.pubkey(),
lamports, lamports,
keys.clone(), keys.clone(),
@ -327,13 +338,13 @@ pub fn process_set_validator_info(
keys, keys,
&validator_info, &validator_info,
)]); )]);
let signers = vec![&config.keypair, &info_keypair]; let signers = vec![config.signers[0], &info_keypair];
let message = Message::new(instructions); let message = Message::new(instructions);
(message, signers) (message, signers)
} else { } else {
println!( println!(
"Updating Validator {:?} info at: {:?}", "Updating Validator {:?} info at: {:?}",
config.keypair.pubkey(), config.signers[0].pubkey(),
info_pubkey info_pubkey
); );
let instructions = vec![config_instruction::store( let instructions = vec![config_instruction::store(
@ -342,17 +353,18 @@ pub fn process_set_validator_info(
keys, keys,
&validator_info, &validator_info,
)]; )];
let message = Message::new_with_payer(instructions, Some(&config.keypair.pubkey())); let message = Message::new_with_payer(instructions, Some(&config.signers[0].pubkey()));
let signers = vec![&config.keypair]; let signers = vec![config.signers[0]];
(message, signers) (message, signers)
}; };
// Submit transaction // Submit transaction
let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?;
let mut tx = Transaction::new(&signers, message, recent_blockhash); let mut tx = Transaction::new_unsigned(message);
tx.try_sign(&signers, recent_blockhash)?;
check_account_for_fee( check_account_for_fee(
rpc_client, rpc_client,
&config.keypair.pubkey(), &config.signers[0].pubkey(),
&fee_calculator, &fee_calculator,
&tx.message, &tx.message,
)?; )?;

View File

@ -1,15 +1,16 @@
use crate::cli::{ use crate::cli::{
build_balance_message, check_account_for_fee, check_unique_pubkeys, build_balance_message, check_account_for_fee, check_unique_pubkeys, generate_unique_signers,
log_instruction_custom_error, CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult, log_instruction_custom_error, CliCommand, CliCommandInfo, CliConfig, CliError, CliSignerInfo,
ProcessResult,
}; };
use clap::{value_t_or_exit, App, Arg, ArgMatches, SubCommand}; use clap::{value_t_or_exit, App, Arg, ArgMatches, SubCommand};
use solana_clap_utils::{input_parsers::*, input_validators::*}; use solana_clap_utils::{input_parsers::*, input_validators::*};
use solana_client::rpc_client::RpcClient; use solana_client::rpc_client::RpcClient;
use solana_remote_wallet::remote_wallet::RemoteWalletManager;
use solana_sdk::{ use solana_sdk::{
account::Account, account::Account,
message::Message,
pubkey::Pubkey, pubkey::Pubkey,
signature::Keypair,
signature::Signer,
system_instruction::{create_address_with_seed, SystemError}, system_instruction::{create_address_with_seed, SystemError},
transaction::Transaction, transaction::Transaction,
}; };
@ -17,6 +18,7 @@ use solana_vote_program::{
vote_instruction::{self, VoteError}, vote_instruction::{self, VoteError},
vote_state::{VoteAuthorize, VoteInit, VoteState}, vote_state::{VoteAuthorize, VoteInit, VoteState},
}; };
use std::sync::Arc;
pub trait VoteSubCommands { pub trait VoteSubCommands {
fn vote_subcommands(self) -> Self; fn vote_subcommands(self) -> Self;
@ -175,56 +177,88 @@ impl VoteSubCommands for App<'_, '_> {
} }
} }
pub fn parse_vote_create_account(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliError> { pub fn parse_vote_create_account(
let vote_account = keypair_of(matches, "vote_account").unwrap(); matches: &ArgMatches<'_>,
default_signer_path: &str,
wallet_manager: Option<&Arc<RemoteWalletManager>>,
) -> Result<CliCommandInfo, CliError> {
let (vote_account, _) = signer_of(matches, "vote_account", wallet_manager)?;
let seed = matches.value_of("seed").map(|s| s.to_string()); let seed = matches.value_of("seed").map(|s| s.to_string());
let identity_pubkey = pubkey_of(matches, "identity_pubkey").unwrap(); let identity_pubkey = pubkey_of(matches, "identity_pubkey").unwrap();
let commission = value_t_or_exit!(matches, "commission", u8); let commission = value_t_or_exit!(matches, "commission", u8);
let authorized_voter = pubkey_of(matches, "authorized_voter"); let authorized_voter = pubkey_of(matches, "authorized_voter");
let authorized_withdrawer = pubkey_of(matches, "authorized_withdrawer"); let authorized_withdrawer = pubkey_of(matches, "authorized_withdrawer");
let payer_provided = None;
let CliSignerInfo { signers } = generate_unique_signers(
vec![payer_provided, vote_account],
matches,
default_signer_path,
wallet_manager,
)?;
Ok(CliCommandInfo { Ok(CliCommandInfo {
command: CliCommand::CreateVoteAccount { command: CliCommand::CreateVoteAccount {
vote_account: vote_account.into(),
seed, seed,
node_pubkey: identity_pubkey, node_pubkey: identity_pubkey,
authorized_voter, authorized_voter,
authorized_withdrawer, authorized_withdrawer,
commission, commission,
}, },
require_keypair: true, signers,
}) })
} }
pub fn parse_vote_authorize( pub fn parse_vote_authorize(
matches: &ArgMatches<'_>, matches: &ArgMatches<'_>,
default_signer_path: &str,
wallet_manager: Option<&Arc<RemoteWalletManager>>,
vote_authorize: VoteAuthorize, vote_authorize: VoteAuthorize,
) -> Result<CliCommandInfo, CliError> { ) -> Result<CliCommandInfo, CliError> {
let vote_account_pubkey = pubkey_of(matches, "vote_account_pubkey").unwrap(); let vote_account_pubkey = pubkey_of(matches, "vote_account_pubkey").unwrap();
let new_authorized_pubkey = pubkey_of(matches, "new_authorized_pubkey").unwrap(); let new_authorized_pubkey = pubkey_of(matches, "new_authorized_pubkey").unwrap();
let authorized_voter_provided = None;
let CliSignerInfo { signers } = generate_unique_signers(
vec![authorized_voter_provided],
matches,
default_signer_path,
wallet_manager,
)?;
Ok(CliCommandInfo { Ok(CliCommandInfo {
command: CliCommand::VoteAuthorize { command: CliCommand::VoteAuthorize {
vote_account_pubkey, vote_account_pubkey,
new_authorized_pubkey, new_authorized_pubkey,
vote_authorize, vote_authorize,
}, },
require_keypair: true, signers,
}) })
} }
pub fn parse_vote_update_validator(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliError> { pub fn parse_vote_update_validator(
matches: &ArgMatches<'_>,
default_signer_path: &str,
wallet_manager: Option<&Arc<RemoteWalletManager>>,
) -> Result<CliCommandInfo, CliError> {
let vote_account_pubkey = pubkey_of(matches, "vote_account_pubkey").unwrap(); let vote_account_pubkey = pubkey_of(matches, "vote_account_pubkey").unwrap();
let new_identity_pubkey = pubkey_of(matches, "new_identity_pubkey").unwrap(); let new_identity_pubkey = pubkey_of(matches, "new_identity_pubkey").unwrap();
let authorized_voter = keypair_of(matches, "authorized_voter").unwrap(); let (authorized_voter, _) = signer_of(matches, "authorized_voter", wallet_manager)?;
let payer_provided = None;
let CliSignerInfo { signers } = generate_unique_signers(
vec![payer_provided, authorized_voter],
matches,
default_signer_path,
wallet_manager,
)?;
Ok(CliCommandInfo { Ok(CliCommandInfo {
command: CliCommand::VoteUpdateValidator { command: CliCommand::VoteUpdateValidator {
vote_account_pubkey, vote_account_pubkey,
new_identity_pubkey, new_identity_pubkey,
authorized_voter: authorized_voter.into(),
}, },
require_keypair: true, signers,
}) })
} }
@ -238,20 +272,20 @@ pub fn parse_vote_get_account_command(
pubkey: vote_account_pubkey, pubkey: vote_account_pubkey,
use_lamports_unit, use_lamports_unit,
}, },
require_keypair: false, signers: vec![],
}) })
} }
pub fn process_create_vote_account( pub fn process_create_vote_account(
rpc_client: &RpcClient, rpc_client: &RpcClient,
config: &CliConfig, config: &CliConfig,
vote_account: &Keypair,
seed: &Option<String>, seed: &Option<String>,
identity_pubkey: &Pubkey, identity_pubkey: &Pubkey,
authorized_voter: &Option<Pubkey>, authorized_voter: &Option<Pubkey>,
authorized_withdrawer: &Option<Pubkey>, authorized_withdrawer: &Option<Pubkey>,
commission: u8, commission: u8,
) -> ProcessResult { ) -> ProcessResult {
let vote_account = config.signers[1];
let vote_account_pubkey = vote_account.pubkey(); let vote_account_pubkey = vote_account.pubkey();
let vote_account_address = if let Some(seed) = seed { let vote_account_address = if let Some(seed) = seed {
create_address_with_seed(&vote_account_pubkey, &seed, &solana_vote_program::id())? create_address_with_seed(&vote_account_pubkey, &seed, &solana_vote_program::id())?
@ -259,7 +293,7 @@ pub fn process_create_vote_account(
vote_account_pubkey vote_account_pubkey
}; };
check_unique_pubkeys( check_unique_pubkeys(
(&config.keypair.pubkey(), "cli keypair".to_string()), (&config.signers[0].pubkey(), "cli keypair".to_string()),
(&vote_account_address, "vote_account".to_string()), (&vote_account_address, "vote_account".to_string()),
)?; )?;
@ -293,7 +327,7 @@ pub fn process_create_vote_account(
let ixs = if let Some(seed) = seed { let ixs = if let Some(seed) = seed {
vote_instruction::create_account_with_seed( vote_instruction::create_account_with_seed(
&config.keypair.pubkey(), // from &config.signers[0].pubkey(), // from
&vote_account_address, // to &vote_account_address, // to
&vote_account_pubkey, // base &vote_account_pubkey, // base
seed, // seed seed, // seed
@ -302,7 +336,7 @@ pub fn process_create_vote_account(
) )
} else { } else {
vote_instruction::create_account( vote_instruction::create_account(
&config.keypair.pubkey(), &config.signers[0].pubkey(),
&vote_account_pubkey, &vote_account_pubkey,
&vote_init, &vote_init,
required_balance, required_balance,
@ -310,20 +344,16 @@ pub fn process_create_vote_account(
}; };
let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?;
let signers = if vote_account_pubkey != config.keypair.pubkey() { let message = Message::new(ixs);
vec![&config.keypair, vote_account] // both must sign if `from` and `to` differ let mut tx = Transaction::new_unsigned(message);
} else { tx.try_sign(&config.signers, recent_blockhash)?;
vec![&config.keypair] // when stake_account == config.keypair and there's a seed, we only need one signature
};
let mut tx = Transaction::new_signed_instructions(&signers, ixs, recent_blockhash);
check_account_for_fee( check_account_for_fee(
rpc_client, rpc_client,
&config.keypair.pubkey(), &config.signers[0].pubkey(),
&fee_calculator, &fee_calculator,
&tx.message, &tx.message,
)?; )?;
let result = rpc_client.send_and_confirm_transaction(&mut tx, &signers); let result = rpc_client.send_and_confirm_transaction(&mut tx, &config.signers);
log_instruction_custom_error::<SystemError>(result) log_instruction_custom_error::<SystemError>(result)
} }
@ -341,24 +371,21 @@ pub fn process_vote_authorize(
let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?;
let ixs = vec![vote_instruction::authorize( let ixs = vec![vote_instruction::authorize(
vote_account_pubkey, // vote account to update vote_account_pubkey, // vote account to update
&config.keypair.pubkey(), // current authorized voter &config.signers[0].pubkey(), // current authorized voter
new_authorized_pubkey, // new vote signer/withdrawer new_authorized_pubkey, // new vote signer/withdrawer
vote_authorize, // vote or withdraw vote_authorize, // vote or withdraw
)]; )];
let mut tx = Transaction::new_signed_with_payer( let message = Message::new_with_payer(ixs, Some(&config.signers[0].pubkey()));
ixs, let mut tx = Transaction::new_unsigned(message);
Some(&config.keypair.pubkey()), tx.try_sign(&config.signers, recent_blockhash)?;
&[&config.keypair],
recent_blockhash,
);
check_account_for_fee( check_account_for_fee(
rpc_client, rpc_client,
&config.keypair.pubkey(), &config.signers[0].pubkey(),
&fee_calculator, &fee_calculator,
&tx.message, &tx.message,
)?; )?;
let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair]); let result = rpc_client.send_and_confirm_transaction(&mut tx, &[config.signers[0]]);
log_instruction_custom_error::<VoteError>(result) log_instruction_custom_error::<VoteError>(result)
} }
@ -367,8 +394,8 @@ pub fn process_vote_update_validator(
config: &CliConfig, config: &CliConfig,
vote_account_pubkey: &Pubkey, vote_account_pubkey: &Pubkey,
new_identity_pubkey: &Pubkey, new_identity_pubkey: &Pubkey,
authorized_voter: &Keypair,
) -> ProcessResult { ) -> ProcessResult {
let authorized_voter = config.signers[1];
check_unique_pubkeys( check_unique_pubkeys(
(vote_account_pubkey, "vote_account_pubkey".to_string()), (vote_account_pubkey, "vote_account_pubkey".to_string()),
(new_identity_pubkey, "new_identity_pubkey".to_string()), (new_identity_pubkey, "new_identity_pubkey".to_string()),
@ -380,19 +407,16 @@ pub fn process_vote_update_validator(
new_identity_pubkey, new_identity_pubkey,
)]; )];
let mut tx = Transaction::new_signed_with_payer( let message = Message::new_with_payer(ixs, Some(&config.signers[0].pubkey()));
ixs, let mut tx = Transaction::new_unsigned(message);
Some(&config.keypair.pubkey()), tx.try_sign(&config.signers, recent_blockhash)?;
&[&config.keypair, authorized_voter],
recent_blockhash,
);
check_account_for_fee( check_account_for_fee(
rpc_client, rpc_client,
&config.keypair.pubkey(), &config.signers[0].pubkey(),
&fee_calculator, &fee_calculator,
&tx.message, &tx.message,
)?; )?;
let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair]); let result = rpc_client.send_and_confirm_transaction(&mut tx, &config.signers);
log_instruction_custom_error::<VoteError>(result) log_instruction_custom_error::<VoteError>(result)
} }
@ -474,7 +498,7 @@ pub fn process_show_vote_account(
mod tests { mod tests {
use super::*; use super::*;
use crate::cli::{app, parse_command}; use crate::cli::{app, parse_command};
use solana_sdk::signature::write_keypair; use solana_sdk::signature::{read_keypair_file, write_keypair, Keypair, Signer};
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
fn make_tmp_file() -> (String, NamedTempFile) { fn make_tmp_file() -> (String, NamedTempFile) {
@ -492,6 +516,10 @@ mod tests {
let pubkey2 = keypair2.pubkey(); let pubkey2 = keypair2.pubkey();
let pubkey2_string = pubkey2.to_string(); let pubkey2_string = pubkey2.to_string();
let default_keypair = Keypair::new();
let (default_keypair_file, mut tmp_file) = make_tmp_file();
write_keypair(&default_keypair, tmp_file.as_file_mut()).unwrap();
let test_authorize_voter = test_commands.clone().get_matches_from(vec![ let test_authorize_voter = test_commands.clone().get_matches_from(vec![
"test", "test",
"vote-authorize-voter", "vote-authorize-voter",
@ -499,14 +527,14 @@ mod tests {
&pubkey2_string, &pubkey2_string,
]); ]);
assert_eq!( assert_eq!(
parse_command(&test_authorize_voter).unwrap(), parse_command(&test_authorize_voter, &default_keypair_file, None).unwrap(),
CliCommandInfo { CliCommandInfo {
command: CliCommand::VoteAuthorize { command: CliCommand::VoteAuthorize {
vote_account_pubkey: pubkey, vote_account_pubkey: pubkey,
new_authorized_pubkey: pubkey2, new_authorized_pubkey: pubkey2,
vote_authorize: VoteAuthorize::Voter vote_authorize: VoteAuthorize::Voter
}, },
require_keypair: true signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()],
} }
); );
@ -525,17 +553,19 @@ mod tests {
"10", "10",
]); ]);
assert_eq!( assert_eq!(
parse_command(&test_create_vote_account).unwrap(), parse_command(&test_create_vote_account, &default_keypair_file, None).unwrap(),
CliCommandInfo { CliCommandInfo {
command: CliCommand::CreateVoteAccount { command: CliCommand::CreateVoteAccount {
vote_account: keypair.into(),
seed: None, seed: None,
node_pubkey, node_pubkey,
authorized_voter: None, authorized_voter: None,
authorized_withdrawer: None, authorized_withdrawer: None,
commission: 10, commission: 10,
}, },
require_keypair: true signers: vec![
read_keypair_file(&default_keypair_file).unwrap().into(),
Box::new(keypair)
],
} }
); );
@ -550,17 +580,19 @@ mod tests {
&node_pubkey_string, &node_pubkey_string,
]); ]);
assert_eq!( assert_eq!(
parse_command(&test_create_vote_account2).unwrap(), parse_command(&test_create_vote_account2, &default_keypair_file, None).unwrap(),
CliCommandInfo { CliCommandInfo {
command: CliCommand::CreateVoteAccount { command: CliCommand::CreateVoteAccount {
vote_account: keypair.into(),
seed: None, seed: None,
node_pubkey, node_pubkey,
authorized_voter: None, authorized_voter: None,
authorized_withdrawer: None, authorized_withdrawer: None,
commission: 100, commission: 100,
}, },
require_keypair: true signers: vec![
read_keypair_file(&default_keypair_file).unwrap().into(),
Box::new(keypair)
],
} }
); );
@ -579,17 +611,19 @@ mod tests {
&authed.to_string(), &authed.to_string(),
]); ]);
assert_eq!( assert_eq!(
parse_command(&test_create_vote_account3).unwrap(), parse_command(&test_create_vote_account3, &default_keypair_file, None).unwrap(),
CliCommandInfo { CliCommandInfo {
command: CliCommand::CreateVoteAccount { command: CliCommand::CreateVoteAccount {
vote_account: keypair.into(),
seed: None, seed: None,
node_pubkey, node_pubkey,
authorized_voter: Some(authed), authorized_voter: Some(authed),
authorized_withdrawer: None, authorized_withdrawer: None,
commission: 100 commission: 100
}, },
require_keypair: true signers: vec![
read_keypair_file(&default_keypair_file).unwrap().into(),
Box::new(keypair)
],
} }
); );
@ -606,17 +640,19 @@ mod tests {
&authed.to_string(), &authed.to_string(),
]); ]);
assert_eq!( assert_eq!(
parse_command(&test_create_vote_account4).unwrap(), parse_command(&test_create_vote_account4, &default_keypair_file, None).unwrap(),
CliCommandInfo { CliCommandInfo {
command: CliCommand::CreateVoteAccount { command: CliCommand::CreateVoteAccount {
vote_account: keypair.into(),
seed: None, seed: None,
node_pubkey, node_pubkey,
authorized_voter: None, authorized_voter: None,
authorized_withdrawer: Some(authed), authorized_withdrawer: Some(authed),
commission: 100 commission: 100
}, },
require_keypair: true signers: vec![
read_keypair_file(&default_keypair_file).unwrap().into(),
Box::new(keypair)
],
} }
); );
@ -628,16 +664,16 @@ mod tests {
&keypair_file, &keypair_file,
]); ]);
assert_eq!( assert_eq!(
parse_command(&test_update_validator).unwrap(), parse_command(&test_update_validator, &default_keypair_file, None).unwrap(),
CliCommandInfo { CliCommandInfo {
command: CliCommand::VoteUpdateValidator { command: CliCommand::VoteUpdateValidator {
vote_account_pubkey: pubkey, vote_account_pubkey: pubkey,
new_identity_pubkey: pubkey2, new_identity_pubkey: pubkey2,
authorized_voter: solana_sdk::signature::read_keypair_file(&keypair_file)
.unwrap()
.into(),
}, },
require_keypair: true signers: vec![
read_keypair_file(&default_keypair_file).unwrap().into(),
Box::new(read_keypair_file(&keypair_file).unwrap())
],
} }
); );
} }

View File

@ -3,7 +3,7 @@ use solana_cli::cli::{process_command, CliCommand, CliConfig};
use solana_client::rpc_client::RpcClient; use solana_client::rpc_client::RpcClient;
use solana_core::validator::new_validator_for_tests; use solana_core::validator::new_validator_for_tests;
use solana_faucet::faucet::run_local_faucet; use solana_faucet::faucet::run_local_faucet;
use solana_sdk::{bpf_loader, pubkey::Pubkey}; use solana_sdk::{bpf_loader, pubkey::Pubkey, signature::Keypair};
use std::{ use std::{
fs::{remove_dir_all, File}, fs::{remove_dir_all, File},
io::Read, io::Read,
@ -38,6 +38,7 @@ fn test_cli_deploy_program() {
.unwrap(); .unwrap();
let mut config = CliConfig::default(); let mut config = CliConfig::default();
let keypair = Keypair::new();
config.json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); config.json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port());
config.command = CliCommand::Airdrop { config.command = CliCommand::Airdrop {
faucet_host: None, faucet_host: None,
@ -45,6 +46,7 @@ fn test_cli_deploy_program() {
pubkey: None, pubkey: None,
lamports: minimum_balance_for_rent_exemption + 1, // min balance for rent exemption + leftover for tx processing lamports: minimum_balance_for_rent_exemption + 1, // min balance for rent exemption + leftover for tx processing
}; };
config.signers = vec![&keypair];
process_command(&config).unwrap(); process_command(&config).unwrap();
config.command = CliCommand::Deploy(pathbuf.to_str().unwrap().to_string()); config.command = CliCommand::Deploy(pathbuf.to_str().unwrap().to_string());

View File

@ -1,29 +1,17 @@
use solana_cli::cli::{ use solana_cli::cli::{process_command, request_and_confirm_airdrop, CliCommand, CliConfig};
process_command, request_and_confirm_airdrop, CliCommand, CliConfig, SigningAuthority,
};
use solana_client::rpc_client::RpcClient; use solana_client::rpc_client::RpcClient;
use solana_faucet::faucet::run_local_faucet; use solana_faucet::faucet::run_local_faucet;
use solana_sdk::{ use solana_sdk::{
hash::Hash, hash::Hash,
pubkey::Pubkey, pubkey::Pubkey,
signature::{read_keypair_file, write_keypair, Keypair, Signer}, signature::{keypair_from_seed, Keypair, Signer},
system_instruction::create_address_with_seed, system_instruction::create_address_with_seed,
system_program, system_program,
}; };
use std::fs::remove_dir_all; use std::{fs::remove_dir_all, sync::mpsc::channel, thread::sleep, time::Duration};
use std::sync::mpsc::channel;
#[cfg(test)] #[cfg(test)]
use solana_core::validator::new_validator_for_tests; use solana_core::validator::new_validator_for_tests;
use std::thread::sleep;
use std::time::Duration;
use tempfile::NamedTempFile;
fn make_tmp_file() -> (String, NamedTempFile) {
let tmp_file = NamedTempFile::new().unwrap();
(String::from(tmp_file.path().to_str().unwrap()), tmp_file)
}
fn check_balance(expected_balance: u64, client: &RpcClient, pubkey: &Pubkey) { fn check_balance(expected_balance: u64, client: &RpcClient, pubkey: &Pubkey) {
(0..5).for_each(|tries| { (0..5).for_each(|tries| {
@ -46,26 +34,9 @@ fn test_nonce() {
let faucet_addr = receiver.recv().unwrap(); let faucet_addr = receiver.recv().unwrap();
let rpc_client = RpcClient::new_socket(leader_data.rpc); let rpc_client = RpcClient::new_socket(leader_data.rpc);
let json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port());
let mut config_payer = CliConfig::default(); full_battery_tests(&rpc_client, &faucet_addr, json_rpc_url, None, false);
config_payer.json_rpc_url =
format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port());
let mut config_nonce = CliConfig::default();
config_nonce.json_rpc_url =
format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port());
let (keypair_file, mut tmp_file) = make_tmp_file();
write_keypair(&config_nonce.keypair, tmp_file.as_file_mut()).unwrap();
full_battery_tests(
&rpc_client,
&faucet_addr,
&mut config_payer,
&mut config_nonce,
&keypair_file,
None,
None,
);
server.close().unwrap(); server.close().unwrap();
remove_dir_all(ledger_path).unwrap(); remove_dir_all(ledger_path).unwrap();
@ -79,25 +50,14 @@ fn test_nonce_with_seed() {
let faucet_addr = receiver.recv().unwrap(); let faucet_addr = receiver.recv().unwrap();
let rpc_client = RpcClient::new_socket(leader_data.rpc); let rpc_client = RpcClient::new_socket(leader_data.rpc);
let json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port());
let mut config_payer = CliConfig::default();
config_payer.json_rpc_url =
format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port());
let mut config_nonce = CliConfig::default();
config_nonce.json_rpc_url =
format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port());
let (keypair_file, mut tmp_file) = make_tmp_file();
write_keypair(&config_nonce.keypair, tmp_file.as_file_mut()).unwrap();
full_battery_tests( full_battery_tests(
&rpc_client, &rpc_client,
&faucet_addr, &faucet_addr,
&mut config_payer, json_rpc_url,
&mut config_nonce,
&keypair_file,
Some(String::from("seed")), Some(String::from("seed")),
None, false,
); );
server.close().unwrap(); server.close().unwrap();
@ -112,78 +72,73 @@ fn test_nonce_with_authority() {
let faucet_addr = receiver.recv().unwrap(); let faucet_addr = receiver.recv().unwrap();
let rpc_client = RpcClient::new_socket(leader_data.rpc); let rpc_client = RpcClient::new_socket(leader_data.rpc);
let json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port());
let mut config_payer = CliConfig::default(); full_battery_tests(&rpc_client, &faucet_addr, json_rpc_url, None, true);
config_payer.json_rpc_url =
format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port());
let mut config_nonce = CliConfig::default();
config_nonce.json_rpc_url =
format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port());
let (nonce_keypair_file, mut tmp_file) = make_tmp_file();
write_keypair(&config_nonce.keypair, tmp_file.as_file_mut()).unwrap();
let nonce_authority = Keypair::new();
let (authority_keypair_file, mut tmp_file2) = make_tmp_file();
write_keypair(&nonce_authority, tmp_file2.as_file_mut()).unwrap();
full_battery_tests(
&rpc_client,
&faucet_addr,
&mut config_payer,
&mut config_nonce,
&nonce_keypair_file,
None,
Some(&authority_keypair_file),
);
server.close().unwrap(); server.close().unwrap();
remove_dir_all(ledger_path).unwrap(); remove_dir_all(ledger_path).unwrap();
} }
fn read_keypair_from_option(keypair_file: &Option<&str>) -> Option<SigningAuthority> {
keypair_file.map(|akf| read_keypair_file(&akf).unwrap().into())
}
fn full_battery_tests( fn full_battery_tests(
rpc_client: &RpcClient, rpc_client: &RpcClient,
faucet_addr: &std::net::SocketAddr, faucet_addr: &std::net::SocketAddr,
config_payer: &mut CliConfig, json_rpc_url: String,
config_nonce: &mut CliConfig,
nonce_keypair_file: &str,
seed: Option<String>, seed: Option<String>,
authority_keypair_file: Option<&str>, use_nonce_authority: bool,
) { ) {
let mut config_payer = CliConfig::default();
config_payer.json_rpc_url = json_rpc_url.clone();
let payer = Keypair::new();
config_payer.signers = vec![&payer];
request_and_confirm_airdrop( request_and_confirm_airdrop(
&rpc_client, &rpc_client,
&faucet_addr, &faucet_addr,
&config_payer.keypair.pubkey(), &config_payer.signers[0].pubkey(),
2000, 2000,
) )
.unwrap(); .unwrap();
check_balance(2000, &rpc_client, &config_payer.keypair.pubkey()); check_balance(2000, &rpc_client, &config_payer.signers[0].pubkey());
let mut config_nonce = CliConfig::default();
config_nonce.json_rpc_url = json_rpc_url;
let nonce_keypair = keypair_from_seed(&[0u8; 32]).unwrap();
config_nonce.signers = vec![&nonce_keypair];
let nonce_account = if let Some(seed) = seed.as_ref() { let nonce_account = if let Some(seed) = seed.as_ref() {
create_address_with_seed(&config_nonce.keypair.pubkey(), seed, &system_program::id()) create_address_with_seed(
&config_nonce.signers[0].pubkey(),
seed,
&system_program::id(),
)
.unwrap() .unwrap()
} else { } else {
config_nonce.keypair.pubkey() nonce_keypair.pubkey()
};
let nonce_authority = Keypair::new();
let optional_authority = if use_nonce_authority {
Some(nonce_authority.pubkey())
} else {
None
}; };
// Create nonce account // Create nonce account
config_payer.signers.push(&nonce_keypair);
config_payer.command = CliCommand::CreateNonceAccount { config_payer.command = CliCommand::CreateNonceAccount {
nonce_account: read_keypair_file(&nonce_keypair_file).unwrap().into(), nonce_account: 1,
seed, seed,
nonce_authority: read_keypair_from_option(&authority_keypair_file) nonce_authority: optional_authority,
.map(|na: SigningAuthority| na.pubkey()),
lamports: 1000, lamports: 1000,
}; };
process_command(&config_payer).unwrap(); process_command(&config_payer).unwrap();
check_balance(1000, &rpc_client, &config_payer.keypair.pubkey()); check_balance(1000, &rpc_client, &config_payer.signers[0].pubkey());
check_balance(1000, &rpc_client, &nonce_account); check_balance(1000, &rpc_client, &nonce_account);
// Get nonce // Get nonce
config_payer.signers.pop();
config_payer.command = CliCommand::GetNonce(nonce_account); config_payer.command = CliCommand::GetNonce(nonce_account);
let first_nonce_string = process_command(&config_payer).unwrap(); let first_nonce_string = process_command(&config_payer).unwrap();
let first_nonce = first_nonce_string.parse::<Hash>().unwrap(); let first_nonce = first_nonce_string.parse::<Hash>().unwrap();
@ -195,14 +150,24 @@ fn full_battery_tests(
assert_eq!(first_nonce, second_nonce); assert_eq!(first_nonce, second_nonce);
let mut authorized_signers: Vec<&dyn Signer> = vec![&payer];
let index = if use_nonce_authority {
authorized_signers.push(&nonce_authority);
1
} else {
0
};
// New nonce // New nonce
config_payer.signers = authorized_signers.clone();
config_payer.command = CliCommand::NewNonce { config_payer.command = CliCommand::NewNonce {
nonce_account, nonce_account,
nonce_authority: read_keypair_from_option(&authority_keypair_file), nonce_authority: index,
}; };
process_command(&config_payer).unwrap(); process_command(&config_payer).unwrap();
// Get nonce // Get nonce
config_payer.signers = vec![&payer];
config_payer.command = CliCommand::GetNonce(nonce_account); config_payer.command = CliCommand::GetNonce(nonce_account);
let third_nonce_string = process_command(&config_payer).unwrap(); let third_nonce_string = process_command(&config_payer).unwrap();
let third_nonce = third_nonce_string.parse::<Hash>().unwrap(); let third_nonce = third_nonce_string.parse::<Hash>().unwrap();
@ -211,14 +176,15 @@ fn full_battery_tests(
// Withdraw from nonce account // Withdraw from nonce account
let payee_pubkey = Pubkey::new_rand(); let payee_pubkey = Pubkey::new_rand();
config_payer.signers = authorized_signers;
config_payer.command = CliCommand::WithdrawFromNonceAccount { config_payer.command = CliCommand::WithdrawFromNonceAccount {
nonce_account, nonce_account,
nonce_authority: read_keypair_from_option(&authority_keypair_file), nonce_authority: index,
destination_account_pubkey: payee_pubkey, destination_account_pubkey: payee_pubkey,
lamports: 100, lamports: 100,
}; };
process_command(&config_payer).unwrap(); process_command(&config_payer).unwrap();
check_balance(1000, &rpc_client, &config_payer.keypair.pubkey()); check_balance(1000, &rpc_client, &config_payer.signers[0].pubkey());
check_balance(900, &rpc_client, &nonce_account); check_balance(900, &rpc_client, &nonce_account);
check_balance(100, &rpc_client, &payee_pubkey); check_balance(100, &rpc_client, &payee_pubkey);
@ -231,48 +197,37 @@ fn full_battery_tests(
// Set new authority // Set new authority
let new_authority = Keypair::new(); let new_authority = Keypair::new();
let (new_authority_keypair_file, mut tmp_file) = make_tmp_file();
write_keypair(&new_authority, tmp_file.as_file_mut()).unwrap();
config_payer.command = CliCommand::AuthorizeNonceAccount { config_payer.command = CliCommand::AuthorizeNonceAccount {
nonce_account, nonce_account,
nonce_authority: read_keypair_from_option(&authority_keypair_file), nonce_authority: index,
new_authority: read_keypair_file(&new_authority_keypair_file) new_authority: new_authority.pubkey(),
.unwrap()
.pubkey(),
}; };
process_command(&config_payer).unwrap(); process_command(&config_payer).unwrap();
// Old authority fails now // Old authority fails now
config_payer.command = CliCommand::NewNonce { config_payer.command = CliCommand::NewNonce {
nonce_account, nonce_account,
nonce_authority: read_keypair_from_option(&authority_keypair_file), nonce_authority: index,
}; };
process_command(&config_payer).unwrap_err(); process_command(&config_payer).unwrap_err();
// New authority can advance nonce // New authority can advance nonce
config_payer.signers = vec![&payer, &new_authority];
config_payer.command = CliCommand::NewNonce { config_payer.command = CliCommand::NewNonce {
nonce_account, nonce_account,
nonce_authority: Some( nonce_authority: 1,
read_keypair_file(&new_authority_keypair_file)
.unwrap()
.into(),
),
}; };
process_command(&config_payer).unwrap(); process_command(&config_payer).unwrap();
// New authority can withdraw from nonce account // New authority can withdraw from nonce account
config_payer.command = CliCommand::WithdrawFromNonceAccount { config_payer.command = CliCommand::WithdrawFromNonceAccount {
nonce_account, nonce_account,
nonce_authority: Some( nonce_authority: 1,
read_keypair_file(&new_authority_keypair_file)
.unwrap()
.into(),
),
destination_account_pubkey: payee_pubkey, destination_account_pubkey: payee_pubkey,
lamports: 100, lamports: 100,
}; };
process_command(&config_payer).unwrap(); process_command(&config_payer).unwrap();
check_balance(1000, &rpc_client, &config_payer.keypair.pubkey()); check_balance(1000, &rpc_client, &config_payer.signers[0].pubkey());
check_balance(800, &rpc_client, &nonce_account); check_balance(800, &rpc_client, &nonce_account);
check_balance(200, &rpc_client, &payee_pubkey); check_balance(200, &rpc_client, &payee_pubkey);
} }

View File

@ -1,5 +1,6 @@
use chrono::prelude::*; use chrono::prelude::*;
use serde_json::Value; use serde_json::Value;
use solana_clap_utils::keypair::presigner_from_pubkey_sigs;
use solana_cli::{ use solana_cli::{
cli::{process_command, request_and_confirm_airdrop, CliCommand, CliConfig, PayCommand}, cli::{process_command, request_and_confirm_airdrop, CliCommand, CliConfig, PayCommand},
offline::{parse_sign_only_reply_string, BlockhashQuery}, offline::{parse_sign_only_reply_string, BlockhashQuery},
@ -11,21 +12,12 @@ use solana_sdk::{
fee_calculator::FeeCalculator, fee_calculator::FeeCalculator,
nonce_state::NonceState, nonce_state::NonceState,
pubkey::Pubkey, pubkey::Pubkey,
signature::{read_keypair_file, write_keypair, Keypair, Signer}, signature::{Keypair, Signer},
}; };
use std::fs::remove_dir_all; use std::{fs::remove_dir_all, sync::mpsc::channel, thread::sleep, time::Duration};
use std::sync::mpsc::channel;
#[cfg(test)] #[cfg(test)]
use solana_core::validator::new_validator_for_tests; use solana_core::validator::new_validator_for_tests;
use std::thread::sleep;
use std::time::Duration;
use tempfile::NamedTempFile;
fn make_tmp_file() -> (String, NamedTempFile) {
let tmp_file = NamedTempFile::new().unwrap();
(String::from(tmp_file.path().to_str().unwrap()), tmp_file)
}
fn check_balance(expected_balance: u64, client: &RpcClient, pubkey: &Pubkey) { fn check_balance(expected_balance: u64, client: &RpcClient, pubkey: &Pubkey) {
(0..5).for_each(|tries| { (0..5).for_each(|tries| {
@ -50,32 +42,36 @@ fn test_cli_timestamp_tx() {
let faucet_addr = receiver.recv().unwrap(); let faucet_addr = receiver.recv().unwrap();
let rpc_client = RpcClient::new_socket(leader_data.rpc); let rpc_client = RpcClient::new_socket(leader_data.rpc);
let default_signer0 = Keypair::new();
let default_signer1 = Keypair::new();
let mut config_payer = CliConfig::default(); let mut config_payer = CliConfig::default();
config_payer.json_rpc_url = config_payer.json_rpc_url =
format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port());
config_payer.signers = vec![&default_signer0];
let mut config_witness = CliConfig::default(); let mut config_witness = CliConfig::default();
config_witness.json_rpc_url = config_payer.json_rpc_url.clone(); config_witness.json_rpc_url = config_payer.json_rpc_url.clone();
config_witness.signers = vec![&default_signer1];
assert_ne!( assert_ne!(
config_payer.keypair.pubkey(), config_payer.signers[0].pubkey(),
config_witness.keypair.pubkey() config_witness.signers[0].pubkey()
); );
request_and_confirm_airdrop( request_and_confirm_airdrop(
&rpc_client, &rpc_client,
&faucet_addr, &faucet_addr,
&config_payer.keypair.pubkey(), &config_payer.signers[0].pubkey(),
50, 50,
) )
.unwrap(); .unwrap();
check_balance(50, &rpc_client, &config_payer.keypair.pubkey()); check_balance(50, &rpc_client, &config_payer.signers[0].pubkey());
request_and_confirm_airdrop( request_and_confirm_airdrop(
&rpc_client, &rpc_client,
&faucet_addr, &faucet_addr,
&config_witness.keypair.pubkey(), &config_witness.signers[0].pubkey(),
1, 1,
) )
.unwrap(); .unwrap();
@ -87,7 +83,7 @@ fn test_cli_timestamp_tx() {
lamports: 10, lamports: 10,
to: bob_pubkey, to: bob_pubkey,
timestamp: Some(dt), timestamp: Some(dt),
timestamp_pubkey: Some(config_witness.keypair.pubkey()), timestamp_pubkey: Some(config_witness.signers[0].pubkey()),
..PayCommand::default() ..PayCommand::default()
}); });
let sig_response = process_command(&config_payer); let sig_response = process_command(&config_payer);
@ -99,7 +95,7 @@ fn test_cli_timestamp_tx() {
.expect("base58-encoded public key"); .expect("base58-encoded public key");
let process_id = Pubkey::new(&process_id_vec); let process_id = Pubkey::new(&process_id_vec);
check_balance(40, &rpc_client, &config_payer.keypair.pubkey()); // config_payer balance check_balance(40, &rpc_client, &config_payer.signers[0].pubkey()); // config_payer balance
check_balance(10, &rpc_client, &process_id); // contract balance check_balance(10, &rpc_client, &process_id); // contract balance
check_balance(0, &rpc_client, &bob_pubkey); // recipient balance check_balance(0, &rpc_client, &bob_pubkey); // recipient balance
@ -107,7 +103,7 @@ fn test_cli_timestamp_tx() {
config_witness.command = CliCommand::TimeElapsed(bob_pubkey, process_id, dt); config_witness.command = CliCommand::TimeElapsed(bob_pubkey, process_id, dt);
process_command(&config_witness).unwrap(); process_command(&config_witness).unwrap();
check_balance(40, &rpc_client, &config_payer.keypair.pubkey()); // config_payer balance check_balance(40, &rpc_client, &config_payer.signers[0].pubkey()); // config_payer balance
check_balance(0, &rpc_client, &process_id); // contract balance check_balance(0, &rpc_client, &process_id); // contract balance
check_balance(10, &rpc_client, &bob_pubkey); // recipient balance check_balance(10, &rpc_client, &bob_pubkey); // recipient balance
@ -125,30 +121,34 @@ fn test_cli_witness_tx() {
let faucet_addr = receiver.recv().unwrap(); let faucet_addr = receiver.recv().unwrap();
let rpc_client = RpcClient::new_socket(leader_data.rpc); let rpc_client = RpcClient::new_socket(leader_data.rpc);
let default_signer0 = Keypair::new();
let default_signer1 = Keypair::new();
let mut config_payer = CliConfig::default(); let mut config_payer = CliConfig::default();
config_payer.json_rpc_url = config_payer.json_rpc_url =
format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port());
config_payer.signers = vec![&default_signer0];
let mut config_witness = CliConfig::default(); let mut config_witness = CliConfig::default();
config_witness.json_rpc_url = config_payer.json_rpc_url.clone(); config_witness.json_rpc_url = config_payer.json_rpc_url.clone();
config_witness.signers = vec![&default_signer1];
assert_ne!( assert_ne!(
config_payer.keypair.pubkey(), config_payer.signers[0].pubkey(),
config_witness.keypair.pubkey() config_witness.signers[0].pubkey()
); );
request_and_confirm_airdrop( request_and_confirm_airdrop(
&rpc_client, &rpc_client,
&faucet_addr, &faucet_addr,
&config_payer.keypair.pubkey(), &config_payer.signers[0].pubkey(),
50, 50,
) )
.unwrap(); .unwrap();
request_and_confirm_airdrop( request_and_confirm_airdrop(
&rpc_client, &rpc_client,
&faucet_addr, &faucet_addr,
&config_witness.keypair.pubkey(), &config_witness.signers[0].pubkey(),
1, 1,
) )
.unwrap(); .unwrap();
@ -157,7 +157,7 @@ fn test_cli_witness_tx() {
config_payer.command = CliCommand::Pay(PayCommand { config_payer.command = CliCommand::Pay(PayCommand {
lamports: 10, lamports: 10,
to: bob_pubkey, to: bob_pubkey,
witnesses: Some(vec![config_witness.keypair.pubkey()]), witnesses: Some(vec![config_witness.signers[0].pubkey()]),
..PayCommand::default() ..PayCommand::default()
}); });
let sig_response = process_command(&config_payer); let sig_response = process_command(&config_payer);
@ -169,7 +169,7 @@ fn test_cli_witness_tx() {
.expect("base58-encoded public key"); .expect("base58-encoded public key");
let process_id = Pubkey::new(&process_id_vec); let process_id = Pubkey::new(&process_id_vec);
check_balance(40, &rpc_client, &config_payer.keypair.pubkey()); // config_payer balance check_balance(40, &rpc_client, &config_payer.signers[0].pubkey()); // config_payer balance
check_balance(10, &rpc_client, &process_id); // contract balance check_balance(10, &rpc_client, &process_id); // contract balance
check_balance(0, &rpc_client, &bob_pubkey); // recipient balance check_balance(0, &rpc_client, &bob_pubkey); // recipient balance
@ -177,7 +177,7 @@ fn test_cli_witness_tx() {
config_witness.command = CliCommand::Witness(bob_pubkey, process_id); config_witness.command = CliCommand::Witness(bob_pubkey, process_id);
process_command(&config_witness).unwrap(); process_command(&config_witness).unwrap();
check_balance(40, &rpc_client, &config_payer.keypair.pubkey()); // config_payer balance check_balance(40, &rpc_client, &config_payer.signers[0].pubkey()); // config_payer balance
check_balance(0, &rpc_client, &process_id); // contract balance check_balance(0, &rpc_client, &process_id); // contract balance
check_balance(10, &rpc_client, &bob_pubkey); // recipient balance check_balance(10, &rpc_client, &bob_pubkey); // recipient balance
@ -195,23 +195,27 @@ fn test_cli_cancel_tx() {
let faucet_addr = receiver.recv().unwrap(); let faucet_addr = receiver.recv().unwrap();
let rpc_client = RpcClient::new_socket(leader_data.rpc); let rpc_client = RpcClient::new_socket(leader_data.rpc);
let default_signer0 = Keypair::new();
let default_signer1 = Keypair::new();
let mut config_payer = CliConfig::default(); let mut config_payer = CliConfig::default();
config_payer.json_rpc_url = config_payer.json_rpc_url =
format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port());
config_payer.signers = vec![&default_signer0];
let mut config_witness = CliConfig::default(); let mut config_witness = CliConfig::default();
config_witness.json_rpc_url = config_payer.json_rpc_url.clone(); config_witness.json_rpc_url = config_payer.json_rpc_url.clone();
config_witness.signers = vec![&default_signer1];
assert_ne!( assert_ne!(
config_payer.keypair.pubkey(), config_payer.signers[0].pubkey(),
config_witness.keypair.pubkey() config_witness.signers[0].pubkey()
); );
request_and_confirm_airdrop( request_and_confirm_airdrop(
&rpc_client, &rpc_client,
&faucet_addr, &faucet_addr,
&config_payer.keypair.pubkey(), &config_payer.signers[0].pubkey(),
50, 50,
) )
.unwrap(); .unwrap();
@ -220,7 +224,7 @@ fn test_cli_cancel_tx() {
config_payer.command = CliCommand::Pay(PayCommand { config_payer.command = CliCommand::Pay(PayCommand {
lamports: 10, lamports: 10,
to: bob_pubkey, to: bob_pubkey,
witnesses: Some(vec![config_witness.keypair.pubkey()]), witnesses: Some(vec![config_witness.signers[0].pubkey()]),
cancelable: true, cancelable: true,
..PayCommand::default() ..PayCommand::default()
}); });
@ -233,7 +237,7 @@ fn test_cli_cancel_tx() {
.expect("base58-encoded public key"); .expect("base58-encoded public key");
let process_id = Pubkey::new(&process_id_vec); let process_id = Pubkey::new(&process_id_vec);
check_balance(40, &rpc_client, &config_payer.keypair.pubkey()); // config_payer balance check_balance(40, &rpc_client, &config_payer.signers[0].pubkey()); // config_payer balance
check_balance(10, &rpc_client, &process_id); // contract balance check_balance(10, &rpc_client, &process_id); // contract balance
check_balance(0, &rpc_client, &bob_pubkey); // recipient balance check_balance(0, &rpc_client, &bob_pubkey); // recipient balance
@ -241,7 +245,7 @@ fn test_cli_cancel_tx() {
config_payer.command = CliCommand::Cancel(process_id); config_payer.command = CliCommand::Cancel(process_id);
process_command(&config_payer).unwrap(); process_command(&config_payer).unwrap();
check_balance(50, &rpc_client, &config_payer.keypair.pubkey()); // config_payer balance check_balance(50, &rpc_client, &config_payer.signers[0].pubkey()); // config_payer balance
check_balance(0, &rpc_client, &process_id); // contract balance check_balance(0, &rpc_client, &process_id); // contract balance
check_balance(0, &rpc_client, &bob_pubkey); // recipient balance check_balance(0, &rpc_client, &bob_pubkey); // recipient balance
@ -259,22 +263,26 @@ fn test_offline_pay_tx() {
let faucet_addr = receiver.recv().unwrap(); let faucet_addr = receiver.recv().unwrap();
let rpc_client = RpcClient::new_socket(leader_data.rpc); let rpc_client = RpcClient::new_socket(leader_data.rpc);
let default_signer = Keypair::new();
let default_offline_signer = Keypair::new();
let mut config_offline = CliConfig::default(); let mut config_offline = CliConfig::default();
config_offline.json_rpc_url = config_offline.json_rpc_url =
format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port());
config_offline.signers = vec![&default_offline_signer];
let mut config_online = CliConfig::default(); let mut config_online = CliConfig::default();
config_online.json_rpc_url = config_online.json_rpc_url =
format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port());
config_online.signers = vec![&default_signer];
assert_ne!( assert_ne!(
config_offline.keypair.pubkey(), config_offline.signers[0].pubkey(),
config_online.keypair.pubkey() config_online.signers[0].pubkey()
); );
request_and_confirm_airdrop( request_and_confirm_airdrop(
&rpc_client, &rpc_client,
&faucet_addr, &faucet_addr,
&config_offline.keypair.pubkey(), &config_offline.signers[0].pubkey(),
50, 50,
) )
.unwrap(); .unwrap();
@ -282,12 +290,12 @@ fn test_offline_pay_tx() {
request_and_confirm_airdrop( request_and_confirm_airdrop(
&rpc_client, &rpc_client,
&faucet_addr, &faucet_addr,
&config_online.keypair.pubkey(), &config_online.signers[0].pubkey(),
50, 50,
) )
.unwrap(); .unwrap();
check_balance(50, &rpc_client, &config_offline.keypair.pubkey()); check_balance(50, &rpc_client, &config_offline.signers[0].pubkey());
check_balance(50, &rpc_client, &config_online.keypair.pubkey()); check_balance(50, &rpc_client, &config_online.signers[0].pubkey());
let (blockhash, _) = rpc_client.get_recent_blockhash().unwrap(); let (blockhash, _) = rpc_client.get_recent_blockhash().unwrap();
config_offline.command = CliCommand::Pay(PayCommand { config_offline.command = CliCommand::Pay(PayCommand {
@ -299,22 +307,25 @@ fn test_offline_pay_tx() {
}); });
let sig_response = process_command(&config_offline).unwrap(); let sig_response = process_command(&config_offline).unwrap();
check_balance(50, &rpc_client, &config_offline.keypair.pubkey()); check_balance(50, &rpc_client, &config_offline.signers[0].pubkey());
check_balance(50, &rpc_client, &config_online.keypair.pubkey()); check_balance(50, &rpc_client, &config_online.signers[0].pubkey());
check_balance(0, &rpc_client, &bob_pubkey); check_balance(0, &rpc_client, &bob_pubkey);
let (blockhash, signers) = parse_sign_only_reply_string(&sig_response); let (blockhash, signers) = parse_sign_only_reply_string(&sig_response);
let offline_presigner =
presigner_from_pubkey_sigs(&config_offline.signers[0].pubkey(), &signers).unwrap();
let online_pubkey = config_online.signers[0].pubkey();
config_online.signers = vec![&offline_presigner];
config_online.command = CliCommand::Pay(PayCommand { config_online.command = CliCommand::Pay(PayCommand {
lamports: 10, lamports: 10,
to: bob_pubkey, to: bob_pubkey,
signers: Some(signers),
blockhash_query: BlockhashQuery::FeeCalculator(blockhash), blockhash_query: BlockhashQuery::FeeCalculator(blockhash),
..PayCommand::default() ..PayCommand::default()
}); });
process_command(&config_online).unwrap(); process_command(&config_online).unwrap();
check_balance(40, &rpc_client, &config_offline.keypair.pubkey()); check_balance(40, &rpc_client, &config_offline.signers[0].pubkey());
check_balance(50, &rpc_client, &config_online.keypair.pubkey()); check_balance(50, &rpc_client, &online_pubkey);
check_balance(10, &rpc_client, &bob_pubkey); check_balance(10, &rpc_client, &bob_pubkey);
server.close().unwrap(); server.close().unwrap();
@ -331,9 +342,11 @@ fn test_nonced_pay_tx() {
let faucet_addr = receiver.recv().unwrap(); let faucet_addr = receiver.recv().unwrap();
let rpc_client = RpcClient::new_socket(leader_data.rpc); let rpc_client = RpcClient::new_socket(leader_data.rpc);
let default_signer = Keypair::new();
let mut config = CliConfig::default(); let mut config = CliConfig::default();
config.json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); config.json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port());
config.signers = vec![&default_signer];
let minimum_nonce_balance = rpc_client let minimum_nonce_balance = rpc_client
.get_minimum_balance_for_rent_exemption(NonceState::size()) .get_minimum_balance_for_rent_exemption(NonceState::size())
@ -342,29 +355,28 @@ fn test_nonced_pay_tx() {
request_and_confirm_airdrop( request_and_confirm_airdrop(
&rpc_client, &rpc_client,
&faucet_addr, &faucet_addr,
&config.keypair.pubkey(), &config.signers[0].pubkey(),
50 + minimum_nonce_balance, 50 + minimum_nonce_balance,
) )
.unwrap(); .unwrap();
check_balance( check_balance(
50 + minimum_nonce_balance, 50 + minimum_nonce_balance,
&rpc_client, &rpc_client,
&config.keypair.pubkey(), &config.signers[0].pubkey(),
); );
// Create nonce account // Create nonce account
let nonce_account = Keypair::new(); let nonce_account = Keypair::new();
let (nonce_keypair_file, mut tmp_file) = make_tmp_file();
write_keypair(&nonce_account, tmp_file.as_file_mut()).unwrap();
config.command = CliCommand::CreateNonceAccount { config.command = CliCommand::CreateNonceAccount {
nonce_account: read_keypair_file(&nonce_keypair_file).unwrap().into(), nonce_account: 1,
seed: None, seed: None,
nonce_authority: Some(config.keypair.pubkey()), nonce_authority: Some(config.signers[0].pubkey()),
lamports: minimum_nonce_balance, lamports: minimum_nonce_balance,
}; };
config.signers.push(&nonce_account);
process_command(&config).unwrap(); process_command(&config).unwrap();
check_balance(50, &rpc_client, &config.keypair.pubkey()); check_balance(50, &rpc_client, &config.signers[0].pubkey());
check_balance(minimum_nonce_balance, &rpc_client, &nonce_account.pubkey()); check_balance(minimum_nonce_balance, &rpc_client, &nonce_account.pubkey());
// Fetch nonce hash // Fetch nonce hash
@ -376,6 +388,7 @@ fn test_nonced_pay_tx() {
}; };
let bob_pubkey = Pubkey::new_rand(); let bob_pubkey = Pubkey::new_rand();
config.signers = vec![&default_signer];
config.command = CliCommand::Pay(PayCommand { config.command = CliCommand::Pay(PayCommand {
lamports: 10, lamports: 10,
to: bob_pubkey, to: bob_pubkey,
@ -385,7 +398,7 @@ fn test_nonced_pay_tx() {
}); });
process_command(&config).expect("failed to process pay command"); process_command(&config).expect("failed to process pay command");
check_balance(40, &rpc_client, &config.keypair.pubkey()); check_balance(40, &rpc_client, &config.signers[0].pubkey());
check_balance(10, &rpc_client, &bob_pubkey); check_balance(10, &rpc_client, &bob_pubkey);
// Verify that nonce has been used // Verify that nonce has been used

View File

@ -2,9 +2,8 @@ use solana_cli::cli::{process_command, CliCommand, CliConfig};
use solana_client::rpc_client::RpcClient; use solana_client::rpc_client::RpcClient;
use solana_core::validator::new_validator_for_tests; use solana_core::validator::new_validator_for_tests;
use solana_faucet::faucet::run_local_faucet; use solana_faucet::faucet::run_local_faucet;
use solana_sdk::signature::Signer; use solana_sdk::signature::Keypair;
use std::fs::remove_dir_all; use std::{fs::remove_dir_all, sync::mpsc::channel};
use std::sync::mpsc::channel;
#[test] #[test]
fn test_cli_request_airdrop() { fn test_cli_request_airdrop() {
@ -21,6 +20,8 @@ fn test_cli_request_airdrop() {
pubkey: None, pubkey: None,
lamports: 50, lamports: 50,
}; };
let keypair = Keypair::new();
bob_config.signers = vec![&keypair];
let sig_response = process_command(&bob_config); let sig_response = process_command(&bob_config);
sig_response.unwrap(); sig_response.unwrap();
@ -28,7 +29,7 @@ fn test_cli_request_airdrop() {
let rpc_client = RpcClient::new_socket(leader_data.rpc); let rpc_client = RpcClient::new_socket(leader_data.rpc);
let balance = rpc_client let balance = rpc_client
.retry_get_balance(&bob_config.keypair.pubkey(), 1) .retry_get_balance(&bob_config.signers[0].pubkey(), 1)
.unwrap() .unwrap()
.unwrap(); .unwrap();
assert_eq!(balance, 50); assert_eq!(balance, 50);

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,4 @@
use solana_clap_utils::keypair::presigner_from_pubkey_sigs;
use solana_cli::{ use solana_cli::{
cli::{process_command, request_and_confirm_airdrop, CliCommand, CliConfig}, cli::{process_command, request_and_confirm_airdrop, CliCommand, CliConfig},
offline::{parse_sign_only_reply_string, BlockhashQuery}, offline::{parse_sign_only_reply_string, BlockhashQuery},
@ -9,21 +10,12 @@ use solana_sdk::{
fee_calculator::FeeCalculator, fee_calculator::FeeCalculator,
nonce_state::NonceState, nonce_state::NonceState,
pubkey::Pubkey, pubkey::Pubkey,
signature::{keypair_from_seed, read_keypair_file, write_keypair, Signer}, signature::{keypair_from_seed, Keypair, Signer},
}; };
use std::fs::remove_dir_all; use std::{fs::remove_dir_all, sync::mpsc::channel, thread::sleep, time::Duration};
use std::sync::mpsc::channel;
#[cfg(test)] #[cfg(test)]
use solana_core::validator::new_validator_for_tests_ex; use solana_core::validator::new_validator_for_tests_ex;
use std::thread::sleep;
use std::time::Duration;
use tempfile::NamedTempFile;
fn make_tmp_file() -> (String, NamedTempFile) {
let tmp_file = NamedTempFile::new().unwrap();
(String::from(tmp_file.path().to_str().unwrap()), tmp_file)
}
fn check_balance(expected_balance: u64, client: &RpcClient, pubkey: &Pubkey) { fn check_balance(expected_balance: u64, client: &RpcClient, pubkey: &Pubkey) {
(0..5).for_each(|tries| { (0..5).for_each(|tries| {
@ -48,13 +40,15 @@ fn test_transfer() {
let rpc_client = RpcClient::new_socket(leader_data.rpc); let rpc_client = RpcClient::new_socket(leader_data.rpc);
let default_signer = Keypair::new();
let default_offline_signer = Keypair::new();
let mut config = CliConfig::default(); let mut config = CliConfig::default();
config.json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); config.json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port());
config.signers = vec![&default_signer];
let sender_pubkey = config.keypair.pubkey(); let sender_pubkey = config.signers[0].pubkey();
let recipient_pubkey = Pubkey::new(&[1u8; 32]); let recipient_pubkey = Pubkey::new(&[1u8; 32]);
println!("sender: {:?}", sender_pubkey);
println!("recipient: {:?}", recipient_pubkey);
request_and_confirm_airdrop(&rpc_client, &faucet_addr, &sender_pubkey, 50_000).unwrap(); request_and_confirm_airdrop(&rpc_client, &faucet_addr, &sender_pubkey, 50_000).unwrap();
check_balance(50_000, &rpc_client, &sender_pubkey); check_balance(50_000, &rpc_client, &sender_pubkey);
@ -64,13 +58,12 @@ fn test_transfer() {
config.command = CliCommand::Transfer { config.command = CliCommand::Transfer {
lamports: 10, lamports: 10,
to: recipient_pubkey, to: recipient_pubkey,
from: None, from: 0,
sign_only: false, sign_only: false,
signers: None,
blockhash_query: BlockhashQuery::All, blockhash_query: BlockhashQuery::All,
nonce_account: None, nonce_account: None,
nonce_authority: None, nonce_authority: 0,
fee_payer: None, fee_payer: 0,
}; };
process_command(&config).unwrap(); process_command(&config).unwrap();
check_balance(49_989, &rpc_client, &sender_pubkey); check_balance(49_989, &rpc_client, &sender_pubkey);
@ -78,12 +71,12 @@ fn test_transfer() {
let mut offline = CliConfig::default(); let mut offline = CliConfig::default();
offline.json_rpc_url = String::default(); offline.json_rpc_url = String::default();
offline.signers = vec![&default_offline_signer];
// Verify we cannot contact the cluster // Verify we cannot contact the cluster
offline.command = CliCommand::ClusterVersion; offline.command = CliCommand::ClusterVersion;
process_command(&offline).unwrap_err(); process_command(&offline).unwrap_err();
let offline_pubkey = offline.keypair.pubkey(); let offline_pubkey = offline.signers[0].pubkey();
println!("offline: {:?}", offline_pubkey);
request_and_confirm_airdrop(&rpc_client, &faucet_addr, &offline_pubkey, 50).unwrap(); request_and_confirm_airdrop(&rpc_client, &faucet_addr, &offline_pubkey, 50).unwrap();
check_balance(50, &rpc_client, &offline_pubkey); check_balance(50, &rpc_client, &offline_pubkey);
@ -92,26 +85,26 @@ fn test_transfer() {
offline.command = CliCommand::Transfer { offline.command = CliCommand::Transfer {
lamports: 10, lamports: 10,
to: recipient_pubkey, to: recipient_pubkey,
from: None, from: 0,
sign_only: true, sign_only: true,
signers: None,
blockhash_query: BlockhashQuery::None(blockhash, FeeCalculator::default()), blockhash_query: BlockhashQuery::None(blockhash, FeeCalculator::default()),
nonce_account: None, nonce_account: None,
nonce_authority: None, nonce_authority: 0,
fee_payer: None, fee_payer: 0,
}; };
let sign_only_reply = process_command(&offline).unwrap(); let sign_only_reply = process_command(&offline).unwrap();
let (blockhash, signers) = parse_sign_only_reply_string(&sign_only_reply); let (blockhash, signers) = parse_sign_only_reply_string(&sign_only_reply);
let offline_presigner = presigner_from_pubkey_sigs(&offline_pubkey, &signers).unwrap();
config.signers = vec![&offline_presigner];
config.command = CliCommand::Transfer { config.command = CliCommand::Transfer {
lamports: 10, lamports: 10,
to: recipient_pubkey, to: recipient_pubkey,
from: Some(offline_pubkey.into()), from: 0,
sign_only: false, sign_only: false,
signers: Some(signers),
blockhash_query: BlockhashQuery::FeeCalculator(blockhash), blockhash_query: BlockhashQuery::FeeCalculator(blockhash),
nonce_account: None, nonce_account: None,
nonce_authority: None, nonce_authority: 0,
fee_payer: Some(offline_pubkey.into()), fee_payer: 0,
}; };
process_command(&config).unwrap(); process_command(&config).unwrap();
check_balance(39, &rpc_client, &offline_pubkey); check_balance(39, &rpc_client, &offline_pubkey);
@ -119,13 +112,12 @@ fn test_transfer() {
// Create nonce account // Create nonce account
let nonce_account = keypair_from_seed(&[3u8; 32]).unwrap(); let nonce_account = keypair_from_seed(&[3u8; 32]).unwrap();
let (nonce_account_file, mut tmp_file) = make_tmp_file();
write_keypair(&nonce_account, tmp_file.as_file_mut()).unwrap();
let minimum_nonce_balance = rpc_client let minimum_nonce_balance = rpc_client
.get_minimum_balance_for_rent_exemption(NonceState::size()) .get_minimum_balance_for_rent_exemption(NonceState::size())
.unwrap(); .unwrap();
config.signers = vec![&default_signer, &nonce_account];
config.command = CliCommand::CreateNonceAccount { config.command = CliCommand::CreateNonceAccount {
nonce_account: read_keypair_file(&nonce_account_file).unwrap().into(), nonce_account: 1,
seed: None, seed: None,
nonce_authority: None, nonce_authority: None,
lamports: minimum_nonce_balance, lamports: minimum_nonce_balance,
@ -142,16 +134,16 @@ fn test_transfer() {
}; };
// Nonced transfer // Nonced transfer
config.signers = vec![&default_signer];
config.command = CliCommand::Transfer { config.command = CliCommand::Transfer {
lamports: 10, lamports: 10,
to: recipient_pubkey, to: recipient_pubkey,
from: None, from: 0,
sign_only: false, sign_only: false,
signers: None,
blockhash_query: BlockhashQuery::FeeCalculator(nonce_hash), blockhash_query: BlockhashQuery::FeeCalculator(nonce_hash),
nonce_account: Some(nonce_account.pubkey()), nonce_account: Some(nonce_account.pubkey()),
nonce_authority: None, nonce_authority: 0,
fee_payer: None, fee_payer: 0,
}; };
process_command(&config).unwrap(); process_command(&config).unwrap();
check_balance(49_976 - minimum_nonce_balance, &rpc_client, &sender_pubkey); check_balance(49_976 - minimum_nonce_balance, &rpc_client, &sender_pubkey);
@ -165,9 +157,10 @@ fn test_transfer() {
assert_ne!(nonce_hash, new_nonce_hash); assert_ne!(nonce_hash, new_nonce_hash);
// Assign nonce authority to offline // Assign nonce authority to offline
config.signers = vec![&default_signer];
config.command = CliCommand::AuthorizeNonceAccount { config.command = CliCommand::AuthorizeNonceAccount {
nonce_account: nonce_account.pubkey(), nonce_account: nonce_account.pubkey(),
nonce_authority: None, nonce_authority: 0,
new_authority: offline_pubkey, new_authority: offline_pubkey,
}; };
process_command(&config).unwrap(); process_command(&config).unwrap();
@ -182,29 +175,30 @@ fn test_transfer() {
}; };
// Offline, nonced transfer // Offline, nonced transfer
offline.signers = vec![&default_offline_signer];
offline.command = CliCommand::Transfer { offline.command = CliCommand::Transfer {
lamports: 10, lamports: 10,
to: recipient_pubkey, to: recipient_pubkey,
from: None, from: 0,
sign_only: true, sign_only: true,
signers: None,
blockhash_query: BlockhashQuery::None(nonce_hash, FeeCalculator::default()), blockhash_query: BlockhashQuery::None(nonce_hash, FeeCalculator::default()),
nonce_account: Some(nonce_account.pubkey()), nonce_account: Some(nonce_account.pubkey()),
nonce_authority: None, nonce_authority: 0,
fee_payer: None, fee_payer: 0,
}; };
let sign_only_reply = process_command(&offline).unwrap(); let sign_only_reply = process_command(&offline).unwrap();
let (blockhash, signers) = parse_sign_only_reply_string(&sign_only_reply); let (blockhash, signers) = parse_sign_only_reply_string(&sign_only_reply);
let offline_presigner = presigner_from_pubkey_sigs(&offline_pubkey, &signers).unwrap();
config.signers = vec![&offline_presigner];
config.command = CliCommand::Transfer { config.command = CliCommand::Transfer {
lamports: 10, lamports: 10,
to: recipient_pubkey, to: recipient_pubkey,
from: Some(offline_pubkey.into()), from: 0,
sign_only: false, sign_only: false,
signers: Some(signers),
blockhash_query: BlockhashQuery::FeeCalculator(blockhash), blockhash_query: BlockhashQuery::FeeCalculator(blockhash),
nonce_account: Some(nonce_account.pubkey()), nonce_account: Some(nonce_account.pubkey()),
nonce_authority: Some(offline_pubkey.into()), nonce_authority: 0,
fee_payer: Some(offline_pubkey.into()), fee_payer: 0,
}; };
process_command(&config).unwrap(); process_command(&config).unwrap();
check_balance(28, &rpc_client, &offline_pubkey); check_balance(28, &rpc_client, &offline_pubkey);

View File

@ -6,21 +6,17 @@ use clap::{
}; };
use num_cpus; use num_cpus;
use solana_clap_utils::{ use solana_clap_utils::{
input_parsers::derivation_of,
input_validators::is_derivation, input_validators::is_derivation,
keypair::{ keypair::{
keypair_from_seed_phrase, parse_keypair_path, prompt_passphrase, KeypairUrl, keypair_from_seed_phrase, prompt_passphrase, signer_from_path,
SKIP_SEED_PHRASE_VALIDATION_ARG, SKIP_SEED_PHRASE_VALIDATION_ARG,
}, },
}; };
use solana_cli_config::config::{Config, CONFIG_FILE}; use solana_cli_config::config::{Config, CONFIG_FILE};
use solana_remote_wallet::remote_keypair::generate_remote_keypair; use solana_remote_wallet::remote_wallet::{maybe_wallet_manager, RemoteWalletManager};
use solana_sdk::{ use solana_sdk::{
pubkey::write_pubkey_file, pubkey::write_pubkey_file,
signature::{ signature::{keypair_from_seed, write_keypair, write_keypair_file, Keypair, Signer},
keypair_from_seed, read_keypair, read_keypair_file, write_keypair, write_keypair_file,
Keypair, Signer,
},
}; };
use std::{ use std::{
collections::HashSet, collections::HashSet,
@ -54,6 +50,7 @@ fn check_for_overwrite(outfile: &str, matches: &ArgMatches) {
fn get_keypair_from_matches( fn get_keypair_from_matches(
matches: &ArgMatches, matches: &ArgMatches,
config: Config, config: Config,
wallet_manager: Option<Arc<RemoteWalletManager>>,
) -> Result<Box<dyn Signer>, Box<dyn error::Error>> { ) -> Result<Box<dyn Signer>, Box<dyn error::Error>> {
let mut path = dirs::home_dir().expect("home directory"); let mut path = dirs::home_dir().expect("home directory");
let path = if matches.is_present("keypair") { let path = if matches.is_present("keypair") {
@ -64,26 +61,7 @@ fn get_keypair_from_matches(
path.extend(&[".config", "solana", "id.json"]); path.extend(&[".config", "solana", "id.json"]);
path.to_str().unwrap() path.to_str().unwrap()
}; };
signer_from_path(matches, path, "pubkey recovery", wallet_manager.as_ref())
match parse_keypair_path(path) {
KeypairUrl::Ask => {
let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name);
Ok(Box::new(keypair_from_seed_phrase(
"pubkey recovery",
skip_validation,
false,
)?))
}
KeypairUrl::Filepath(path) => Ok(Box::new(read_keypair_file(&path)?)),
KeypairUrl::Stdin => {
let mut stdin = std::io::stdin();
Ok(Box::new(read_keypair(&mut stdin)?))
}
KeypairUrl::Usb(path) => Ok(Box::new(generate_remote_keypair(
path,
derivation_of(matches, "derivation_path"),
)?)),
}
} }
fn output_keypair( fn output_keypair(
@ -404,9 +382,11 @@ fn main() -> Result<(), Box<dyn error::Error>> {
Config::default() Config::default()
}; };
let wallet_manager = maybe_wallet_manager()?;
match matches.subcommand() { match matches.subcommand() {
("pubkey", Some(matches)) => { ("pubkey", Some(matches)) => {
let pubkey = get_keypair_from_matches(matches, config)?.try_pubkey()?; let pubkey = get_keypair_from_matches(matches, config, wallet_manager)?.try_pubkey()?;
if matches.is_present("outfile") { if matches.is_present("outfile") {
let outfile = matches.value_of("outfile").unwrap(); let outfile = matches.value_of("outfile").unwrap();
@ -583,7 +563,7 @@ fn main() -> Result<(), Box<dyn error::Error>> {
} }
} }
("verify", Some(matches)) => { ("verify", Some(matches)) => {
let keypair = get_keypair_from_matches(matches, config)?; let keypair = get_keypair_from_matches(matches, config, wallet_manager)?;
let test_data = b"test"; let test_data = b"test";
let signature = keypair.try_sign_message(test_data)?; let signature = keypair.try_sign_message(test_data)?;
let pubkey_bs58 = matches.value_of("pubkey").unwrap(); let pubkey_bs58 = matches.value_of("pubkey").unwrap();

View File

@ -17,6 +17,7 @@ parking_lot = "0.7"
semver = "0.9" semver = "0.9"
solana-sdk = { path = "../sdk", version = "0.23.7" } solana-sdk = { path = "../sdk", version = "0.23.7" }
thiserror = "1.0" thiserror = "1.0"
url = "2.1.1"
[features] [features]
default = ["linux-static-hidraw"] default = ["linux-static-hidraw"]

View File

@ -1,5 +1,5 @@
use crate::remote_wallet::{ use crate::remote_wallet::{
initialize_wallet_manager, DerivationPath, RemoteWallet, RemoteWalletError, RemoteWalletInfo, DerivationPath, RemoteWallet, RemoteWalletError, RemoteWalletInfo, RemoteWalletManager,
}; };
use dialoguer::{theme::ColorfulTheme, Select}; use dialoguer::{theme::ColorfulTheme, Select};
use log::*; use log::*;
@ -7,6 +7,8 @@ use semver::Version as FirmwareVersion;
use solana_sdk::{pubkey::Pubkey, signature::Signature}; use solana_sdk::{pubkey::Pubkey, signature::Signature};
use std::{cmp::min, fmt, sync::Arc}; use std::{cmp::min, fmt, sync::Arc};
const HARDENED_BIT: u32 = 1 << 31;
const APDU_TAG: u8 = 0x05; const APDU_TAG: u8 = 0x05;
const APDU_CLA: u8 = 0xe0; const APDU_CLA: u8 = 0xe0;
const APDU_PAYLOAD_HEADER_LEN: usize = 8; const APDU_PAYLOAD_HEADER_LEN: usize = 8;
@ -41,7 +43,7 @@ const HID_PREFIX_ZERO: usize = 0;
mod commands { mod commands {
#[allow(dead_code)] #[allow(dead_code)]
pub const GET_APP_CONFIGURATION: u8 = 0x06; pub const GET_APP_CONFIGURATION: u8 = 0x01;
pub const GET_PUBKEY: u8 = 0x02; pub const GET_PUBKEY: u8 = 0x02;
pub const SIGN_MESSAGE: u8 = 0x03; pub const SIGN_MESSAGE: u8 = 0x03;
} }
@ -74,7 +76,7 @@ impl LedgerWallet {
// * APDU_INS (1 byte) // * APDU_INS (1 byte)
// * APDU_P1 (1 byte) // * APDU_P1 (1 byte)
// * APDU_P2 (1 byte) // * APDU_P2 (1 byte)
// * APDU_LENGTH (1 byte) // * APDU_LENGTH (2 bytes)
// * APDU_Payload (Variable) // * APDU_Payload (Variable)
// //
fn write(&self, command: u8, p1: u8, p2: u8, data: &[u8]) -> Result<(), RemoteWalletError> { fn write(&self, command: u8, p1: u8, p2: u8, data: &[u8]) -> Result<(), RemoteWalletError> {
@ -139,11 +141,6 @@ impl LedgerWallet {
// * Payload (Optional) // * Payload (Optional)
// //
// Payload // Payload
// * APDU Total Length (2 bytes big endian)
// * APDU_CLA (1 byte)
// * APDU_INS (1 byte)
// * APDU_P1 (1 byte)
// * APDU_P2 (1 byte)
// * APDU_LENGTH (1 byte) // * APDU_LENGTH (1 byte)
// * APDU_Payload (Variable) // * APDU_Payload (Variable)
// //
@ -193,6 +190,10 @@ impl LedgerWallet {
match status { match status {
// These need to be aligned with solana Ledger app error codes, and clippy allowance removed // These need to be aligned with solana Ledger app error codes, and clippy allowance removed
0x6700 => Err(RemoteWalletError::Protocol("Incorrect length")), 0x6700 => Err(RemoteWalletError::Protocol("Incorrect length")),
0x6802 => Err(RemoteWalletError::Protocol("Invalid parameter")),
0x6803 => Err(RemoteWalletError::Protocol(
"Overflow: message longer than MAX_MESSAGE_LENGTH",
)),
0x6982 => Err(RemoteWalletError::Protocol( 0x6982 => Err(RemoteWalletError::Protocol(
"Security status not satisfied (Canceled by user)", "Security status not satisfied (Canceled by user)",
)), )),
@ -260,7 +261,7 @@ impl RemoteWallet for LedgerWallet {
.serial_number .serial_number
.clone() .clone()
.unwrap_or_else(|| "Unknown".to_owned()); .unwrap_or_else(|| "Unknown".to_owned());
self.get_pubkey(&DerivationPath::default()) self.get_pubkey(&DerivationPath::default(), false)
.map(|pubkey| RemoteWalletInfo { .map(|pubkey| RemoteWalletInfo {
model, model,
manufacturer, manufacturer,
@ -269,12 +270,16 @@ impl RemoteWallet for LedgerWallet {
}) })
} }
fn get_pubkey(&self, derivation_path: &DerivationPath) -> Result<Pubkey, RemoteWalletError> { fn get_pubkey(
&self,
derivation_path: &DerivationPath,
confirm_key: bool,
) -> Result<Pubkey, RemoteWalletError> {
let derivation_path = extend_and_serialize(derivation_path); let derivation_path = extend_and_serialize(derivation_path);
let key = self.send_apdu( let key = self.send_apdu(
commands::GET_PUBKEY, commands::GET_PUBKEY,
0, // In the naive implementation, default request is for no device confirmation if confirm_key { 1 } else { 0 },
0, 0,
&derivation_path, &derivation_path,
)?; )?;
@ -361,16 +366,20 @@ pub fn is_valid_ledger(vendor_id: u16, product_id: u16) -> bool {
fn extend_and_serialize(derivation_path: &DerivationPath) -> Vec<u8> { fn extend_and_serialize(derivation_path: &DerivationPath) -> Vec<u8> {
let byte = if derivation_path.change.is_some() { let byte = if derivation_path.change.is_some() {
4 4
} else { } else if derivation_path.account.is_some() {
3 3
} else {
2
}; };
let mut concat_derivation = vec![byte]; let mut concat_derivation = vec![byte];
concat_derivation.extend_from_slice(&SOL_DERIVATION_PATH_BE); concat_derivation.extend_from_slice(&SOL_DERIVATION_PATH_BE);
concat_derivation.extend_from_slice(&[0x80, 0]); if let Some(account) = derivation_path.account {
concat_derivation.extend_from_slice(&derivation_path.account.to_be_bytes()); let hardened_account = account | HARDENED_BIT;
concat_derivation.extend_from_slice(&hardened_account.to_be_bytes());
if let Some(change) = derivation_path.change { if let Some(change) = derivation_path.change {
concat_derivation.extend_from_slice(&[0x80, 0]); let hardened_change = change | HARDENED_BIT;
concat_derivation.extend_from_slice(&change.to_be_bytes()); concat_derivation.extend_from_slice(&hardened_change.to_be_bytes());
}
} }
concat_derivation concat_derivation
} }
@ -378,9 +387,8 @@ fn extend_and_serialize(derivation_path: &DerivationPath) -> Vec<u8> {
/// Choose a Ledger wallet based on matching info fields /// Choose a Ledger wallet based on matching info fields
pub fn get_ledger_from_info( pub fn get_ledger_from_info(
info: RemoteWalletInfo, info: RemoteWalletInfo,
wallet_manager: &RemoteWalletManager,
) -> Result<Arc<LedgerWallet>, RemoteWalletError> { ) -> Result<Arc<LedgerWallet>, RemoteWalletError> {
let wallet_manager = initialize_wallet_manager();
let _device_count = wallet_manager.update_devices()?;
let devices = wallet_manager.list_devices(); let devices = wallet_manager.list_devices();
let (pubkeys, device_paths): (Vec<Pubkey>, Vec<String>) = devices let (pubkeys, device_paths): (Vec<Pubkey>, Vec<String>) = devices
.iter() .iter()

View File

@ -1,7 +1,8 @@
use crate::{ use crate::{
ledger::get_ledger_from_info, ledger::get_ledger_from_info,
remote_wallet::{ remote_wallet::{
DerivationPath, RemoteWallet, RemoteWalletError, RemoteWalletInfo, RemoteWalletType, DerivationPath, RemoteWallet, RemoteWalletError, RemoteWalletInfo, RemoteWalletManager,
RemoteWalletType,
}, },
}; };
use solana_sdk::{ use solana_sdk::{
@ -12,24 +13,30 @@ use solana_sdk::{
pub struct RemoteKeypair { pub struct RemoteKeypair {
pub wallet_type: RemoteWalletType, pub wallet_type: RemoteWalletType,
pub derivation_path: DerivationPath, pub derivation_path: DerivationPath,
pub pubkey: Pubkey,
} }
impl RemoteKeypair { impl RemoteKeypair {
pub fn new(wallet_type: RemoteWalletType, derivation_path: DerivationPath) -> Self { pub fn new(
Self { wallet_type: RemoteWalletType,
derivation_path: DerivationPath,
confirm_key: bool,
) -> Result<Self, RemoteWalletError> {
let pubkey = match &wallet_type {
RemoteWalletType::Ledger(wallet) => wallet.get_pubkey(&derivation_path, confirm_key)?,
};
Ok(Self {
wallet_type, wallet_type,
derivation_path, derivation_path,
} pubkey,
})
} }
} }
impl Signer for RemoteKeypair { impl Signer for RemoteKeypair {
fn try_pubkey(&self) -> Result<Pubkey, SignerError> { fn try_pubkey(&self) -> Result<Pubkey, SignerError> {
match &self.wallet_type { Ok(self.pubkey)
RemoteWalletType::Ledger(wallet) => wallet
.get_pubkey(&self.derivation_path)
.map_err(|e| e.into()),
}
} }
fn try_sign_message(&self, message: &[u8]) -> Result<Signature, SignerError> { fn try_sign_message(&self, message: &[u8]) -> Result<Signature, SignerError> {
@ -44,17 +51,20 @@ impl Signer for RemoteKeypair {
pub fn generate_remote_keypair( pub fn generate_remote_keypair(
path: String, path: String,
explicit_derivation_path: Option<DerivationPath>, explicit_derivation_path: Option<DerivationPath>,
wallet_manager: &RemoteWalletManager,
confirm_key: bool,
) -> Result<RemoteKeypair, RemoteWalletError> { ) -> Result<RemoteKeypair, RemoteWalletError> {
let (remote_wallet_info, mut derivation_path) = RemoteWalletInfo::parse_path(path)?; let (remote_wallet_info, mut derivation_path) = RemoteWalletInfo::parse_path(path)?;
if let Some(derivation) = explicit_derivation_path { if let Some(derivation) = explicit_derivation_path {
derivation_path = derivation; derivation_path = derivation;
} }
if remote_wallet_info.manufacturer == "ledger" { if remote_wallet_info.manufacturer == "ledger" {
let ledger = get_ledger_from_info(remote_wallet_info)?; let ledger = get_ledger_from_info(remote_wallet_info, wallet_manager)?;
Ok(RemoteKeypair { Ok(RemoteKeypair::new(
wallet_type: RemoteWalletType::Ledger(ledger), RemoteWalletType::Ledger(ledger),
derivation_path, derivation_path,
}) confirm_key,
)?)
} else { } else {
Err(RemoteWalletError::DeviceTypeMismatch) Err(RemoteWalletError::DeviceTypeMismatch)
} }

View File

@ -12,6 +12,7 @@ use std::{
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use thiserror::Error; use thiserror::Error;
use url::Url;
const HID_GLOBAL_USAGE_PAGE: u16 = 0xFF00; const HID_GLOBAL_USAGE_PAGE: u16 = 0xFF00;
const HID_USB_DEVICE_CLASS: u8 = 0; const HID_USB_DEVICE_CLASS: u8 = 0;
@ -173,7 +174,11 @@ pub trait RemoteWallet {
) -> Result<RemoteWalletInfo, RemoteWalletError>; ) -> Result<RemoteWalletInfo, RemoteWalletError>;
/// Get solana pubkey from a RemoteWallet /// Get solana pubkey from a RemoteWallet
fn get_pubkey(&self, derivation_path: &DerivationPath) -> Result<Pubkey, RemoteWalletError>; fn get_pubkey(
&self,
derivation_path: &DerivationPath,
confirm_key: bool,
) -> Result<Pubkey, RemoteWalletError>;
/// Sign transaction data with wallet managing pubkey at derivation path m/44'/501'/<account>'/<change>'. /// Sign transaction data with wallet managing pubkey at derivation path m/44'/501'/<account>'/<change>'.
fn sign_message( fn sign_message(
@ -211,47 +216,70 @@ pub struct RemoteWalletInfo {
} }
impl RemoteWalletInfo { impl RemoteWalletInfo {
pub fn parse_path(mut path: String) -> Result<(Self, DerivationPath), RemoteWalletError> { pub fn parse_path(path: String) -> Result<(Self, DerivationPath), RemoteWalletError> {
if path.ends_with('/') { let wallet_path = Url::parse(&path).map_err(|e| {
path.pop(); RemoteWalletError::InvalidDerivationPath(format!("parse error: {:?}", e))
})?;
if wallet_path.host_str().is_none() {
return Err(RemoteWalletError::InvalidDerivationPath(
"missing remote wallet type".to_string(),
));
} }
let mut parts = path.split('/');
let mut wallet_info = RemoteWalletInfo::default(); let mut wallet_info = RemoteWalletInfo::default();
let manufacturer = parts.next().unwrap(); wallet_info.manufacturer = wallet_path.host_str().unwrap().to_string();
wallet_info.manufacturer = manufacturer.to_string();
wallet_info.model = parts.next().unwrap_or("").to_string(); if let Some(wallet_id) = wallet_path.path_segments().map(|c| c.collect::<Vec<_>>()) {
wallet_info.pubkey = parts wallet_info.model = wallet_id[0].to_string();
.next() if wallet_id.len() > 1 {
.and_then(|pubkey_str| Pubkey::from_str(pubkey_str).ok()) wallet_info.pubkey = Pubkey::from_str(wallet_id[1]).map_err(|e| {
.unwrap_or_default(); RemoteWalletError::InvalidDerivationPath(format!(
"pubkey from_str error: {:?}",
e
))
})?;
}
}
let mut derivation_path = DerivationPath::default(); let mut derivation_path = DerivationPath::default();
if let Some(purpose) = parts.next() { let mut query_pairs = wallet_path.query_pairs();
if purpose.replace("'", "") != "44" { if query_pairs.count() > 0 {
return Err(RemoteWalletError::InvalidDerivationPath(format!( for _ in 0..query_pairs.count() {
"Incorrect purpose number, found: {}, must be 44", if let Some(mut pair) = query_pairs.next() {
purpose if pair.0 == "key" {
))); let key_path = pair.1.to_mut();
let _key_path = key_path.clone();
if key_path.ends_with('/') {
key_path.pop();
} }
if let Some(coin) = parts.next() { let mut parts = key_path.split('/');
if coin.replace("'", "") != "501" { derivation_path.account = parts
return Err(RemoteWalletError::InvalidDerivationPath(format!( .next()
"Incorrect coin number, found: {}, must be 501", .and_then(|account| account.replace("'", "").parse::<u32>().ok());
coin
)));
}
if let Some(account) = parts.next() {
derivation_path.account = account.replace("'", "").parse::<u16>().unwrap();
derivation_path.change = parts derivation_path.change = parts
.next() .next()
.and_then(|change| change.replace("'", "").parse::<u16>().ok()); .and_then(|change| change.replace("'", "").parse::<u32>().ok());
if parts.next().is_some() {
return Err(RemoteWalletError::InvalidDerivationPath(format!(
"key path `{}` too deep, only <account>/<change> supported",
_key_path
)));
} }
} else { } else {
return Err(RemoteWalletError::InvalidDerivationPath(format!(
"invalid query string `{}={}`, only `key` supported",
pair.0, pair.1
)));
}
}
if query_pairs.next().is_some() {
return Err(RemoteWalletError::InvalidDerivationPath( return Err(RemoteWalletError::InvalidDerivationPath(
"Derivation path too short, missing coin number".to_string(), "invalid query string, extra fields not supported".to_string(),
)); ));
} }
} }
}
Ok((wallet_info, derivation_path)) Ok((wallet_info, derivation_path))
} }
@ -273,18 +301,23 @@ impl RemoteWalletInfo {
#[derive(Default, PartialEq, Clone)] #[derive(Default, PartialEq, Clone)]
pub struct DerivationPath { pub struct DerivationPath {
pub account: u16, pub account: Option<u32>,
pub change: Option<u16>, pub change: Option<u32>,
} }
impl fmt::Debug for DerivationPath { impl fmt::Debug for DerivationPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let account = if let Some(account) = self.account {
format!("/{:?}'", account)
} else {
"".to_string()
};
let change = if let Some(change) = self.change { let change = if let Some(change) = self.change {
format!("/{:?}'", change) format!("/{:?}'", change)
} else { } else {
"".to_string() "".to_string()
}; };
write!(f, "m/44'/501'/{:?}'{}", self.account, change) write!(f, "m/44'/501'{}{}", account, change)
} }
} }
@ -294,9 +327,20 @@ pub fn is_valid_hid_device(usage_page: u16, interface_number: i32) -> bool {
} }
/// Helper to initialize hidapi and RemoteWalletManager /// Helper to initialize hidapi and RemoteWalletManager
pub fn initialize_wallet_manager() -> Arc<RemoteWalletManager> { pub fn initialize_wallet_manager() -> Result<Arc<RemoteWalletManager>, RemoteWalletError> {
let hidapi = Arc::new(Mutex::new(hidapi::HidApi::new().unwrap())); let hidapi = Arc::new(Mutex::new(hidapi::HidApi::new()?));
RemoteWalletManager::new(hidapi) Ok(RemoteWalletManager::new(hidapi))
}
pub fn maybe_wallet_manager() -> Result<Option<Arc<RemoteWalletManager>>, RemoteWalletError> {
let wallet_manager = initialize_wallet_manager()?;
let device_count = wallet_manager.update_devices()?;
if device_count > 0 {
Ok(Some(wallet_manager))
} else {
drop(wallet_manager);
Ok(None)
}
} }
#[cfg(test)] #[cfg(test)]
@ -307,22 +351,7 @@ mod tests {
fn test_parse_path() { fn test_parse_path() {
let pubkey = Pubkey::new_rand(); let pubkey = Pubkey::new_rand();
let (wallet_info, derivation_path) = let (wallet_info, derivation_path) =
RemoteWalletInfo::parse_path(format!("ledger/nano-s/{:?}/44/501/1/2", pubkey)).unwrap(); RemoteWalletInfo::parse_path(format!("usb://ledger/nano-s/{:?}?key=1/2", pubkey))
assert!(wallet_info.matches(&RemoteWalletInfo {
model: "nano-s".to_string(),
manufacturer: "ledger".to_string(),
serial: "".to_string(),
pubkey,
}));
assert_eq!(
derivation_path,
DerivationPath {
account: 1,
change: Some(2),
}
);
let (wallet_info, derivation_path) =
RemoteWalletInfo::parse_path(format!("ledger/nano-s/{:?}/44'/501'/1'/2'", pubkey))
.unwrap(); .unwrap();
assert!(wallet_info.matches(&RemoteWalletInfo { assert!(wallet_info.matches(&RemoteWalletInfo {
model: "nano-s".to_string(), model: "nano-s".to_string(),
@ -333,17 +362,122 @@ mod tests {
assert_eq!( assert_eq!(
derivation_path, derivation_path,
DerivationPath { DerivationPath {
account: 1, account: Some(1),
change: Some(2),
}
);
let (wallet_info, derivation_path) =
RemoteWalletInfo::parse_path(format!("usb://ledger/nano-s/{:?}?key=1'/2'", pubkey))
.unwrap();
assert!(wallet_info.matches(&RemoteWalletInfo {
model: "nano-s".to_string(),
manufacturer: "ledger".to_string(),
serial: "".to_string(),
pubkey,
}));
assert_eq!(
derivation_path,
DerivationPath {
account: Some(1),
change: Some(2),
}
);
let (wallet_info, derivation_path) =
RemoteWalletInfo::parse_path(format!("usb://ledger/nano-s/{:?}?key=1\'/2\'", pubkey))
.unwrap();
assert!(wallet_info.matches(&RemoteWalletInfo {
model: "nano-s".to_string(),
manufacturer: "ledger".to_string(),
serial: "".to_string(),
pubkey,
}));
assert_eq!(
derivation_path,
DerivationPath {
account: Some(1),
change: Some(2),
}
);
let (wallet_info, derivation_path) =
RemoteWalletInfo::parse_path(format!("usb://ledger/nano-s/{:?}?key=1/2/", pubkey))
.unwrap();
assert!(wallet_info.matches(&RemoteWalletInfo {
model: "nano-s".to_string(),
manufacturer: "ledger".to_string(),
serial: "".to_string(),
pubkey,
}));
assert_eq!(
derivation_path,
DerivationPath {
account: Some(1),
change: Some(2),
}
);
let (wallet_info, derivation_path) =
RemoteWalletInfo::parse_path(format!("usb://ledger/nano-s/{:?}?key=1/", pubkey))
.unwrap();
assert!(wallet_info.matches(&RemoteWalletInfo {
model: "nano-s".to_string(),
manufacturer: "ledger".to_string(),
serial: "".to_string(),
pubkey,
}));
assert_eq!(
derivation_path,
DerivationPath {
account: Some(1),
change: None,
}
);
// Test that wallet id need not be complete for key derivation to work
let (wallet_info, derivation_path) =
RemoteWalletInfo::parse_path("usb://ledger/nano-s?key=1".to_string()).unwrap();
assert!(wallet_info.matches(&RemoteWalletInfo {
model: "nano-s".to_string(),
manufacturer: "ledger".to_string(),
serial: "".to_string(),
pubkey: Pubkey::default(),
}));
assert_eq!(
derivation_path,
DerivationPath {
account: Some(1),
change: None,
}
);
let (wallet_info, derivation_path) =
RemoteWalletInfo::parse_path("usb://ledger/?key=1/2".to_string()).unwrap();
assert!(wallet_info.matches(&RemoteWalletInfo {
model: "".to_string(),
manufacturer: "ledger".to_string(),
serial: "".to_string(),
pubkey: Pubkey::default(),
}));
assert_eq!(
derivation_path,
DerivationPath {
account: Some(1),
change: Some(2), change: Some(2),
} }
); );
// Failure cases
assert!( assert!(
RemoteWalletInfo::parse_path(format!("ledger/nano-s/{:?}/43/501/1/2", pubkey)).is_err() RemoteWalletInfo::parse_path("usb://ledger/nano-s/bad-pubkey?key=1/2".to_string())
.is_err()
); );
assert!(RemoteWalletInfo::parse_path("usb://?key=1/2".to_string()).is_err());
assert!(RemoteWalletInfo::parse_path("usb:/ledger?key=1/2".to_string()).is_err());
assert!(RemoteWalletInfo::parse_path("ledger?key=1/2".to_string()).is_err());
assert!(RemoteWalletInfo::parse_path("usb://ledger?key=1/2/3".to_string()).is_err());
// Other query strings cause an error
assert!( assert!(
RemoteWalletInfo::parse_path(format!("ledger/nano-s/{:?}/44/500/1/2", pubkey)).is_err() RemoteWalletInfo::parse_path("usb://ledger/?key=1/2&test=other".to_string()).is_err()
); );
assert!(RemoteWalletInfo::parse_path("usb://ledger/?Key=1/2".to_string()).is_err());
assert!(RemoteWalletInfo::parse_path("usb://ledger/?test=other".to_string()).is_err());
} }
#[test] #[test]

View File

@ -142,6 +142,18 @@ pub trait Signer {
fn try_sign_message(&self, message: &[u8]) -> Result<Signature, SignerError>; fn try_sign_message(&self, message: &[u8]) -> Result<Signature, SignerError>;
} }
impl PartialEq for dyn Signer {
fn eq(&self, other: &dyn Signer) -> bool {
self.pubkey() == other.pubkey()
}
}
impl std::fmt::Debug for dyn Signer {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(fmt, "Signer: {:?}", self.pubkey())
}
}
impl Signer for Keypair { impl Signer for Keypair {
/// Return the public key for the given keypair /// Return the public key for the given keypair
fn pubkey(&self) -> Pubkey { fn pubkey(&self) -> Pubkey {
@ -170,6 +182,15 @@ where
} }
} }
impl<T> From<T> for Box<dyn Signer>
where
T: Signer + 'static,
{
fn from(signer: T) -> Self {
Box::new(signer)
}
}
#[derive(Debug, Error, PartialEq)] #[derive(Debug, Error, PartialEq)]
pub enum SignerError { pub enum SignerError {
#[error("keypair-pubkey mismatch")] #[error("keypair-pubkey mismatch")]
@ -205,15 +226,14 @@ pub enum SignerError {
UserCancel, UserCancel,
} }
#[derive(Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct Presigner { pub struct Presigner {
pubkey: Pubkey, pubkey: Pubkey,
signature: Signature, signature: Signature,
} }
impl Presigner { impl Presigner {
#[allow(dead_code)] pub fn new(pubkey: &Pubkey, signature: &Signature) -> Self {
fn new(pubkey: &Pubkey, signature: &Signature) -> Self {
Self { Self {
pubkey: *pubkey, pubkey: *pubkey,
signature: *signature, signature: *signature,

View File

@ -48,6 +48,34 @@ impl Signers for [Box<dyn Signer>] {
default_keypairs_impl!(); default_keypairs_impl!();
} }
impl Signers for Vec<&dyn Signer> {
default_keypairs_impl!();
}
impl Signers for [&dyn Signer] {
default_keypairs_impl!();
}
impl Signers for [&dyn Signer; 0] {
default_keypairs_impl!();
}
impl Signers for [&dyn Signer; 1] {
default_keypairs_impl!();
}
impl Signers for [&dyn Signer; 2] {
default_keypairs_impl!();
}
impl Signers for [&dyn Signer; 3] {
default_keypairs_impl!();
}
impl Signers for [&dyn Signer; 4] {
default_keypairs_impl!();
}
impl<T: Signer> Signers for [&T; 0] { impl<T: Signer> Signers for [&T; 0] {
default_keypairs_impl!(); default_keypairs_impl!();
} }
@ -111,4 +139,22 @@ mod tests {
vec![Signature::default(), Signature::default()], vec![Signature::default(), Signature::default()],
); );
} }
#[test]
fn test_dyn_keypairs_by_ref_compile() {
let foo = Foo {};
let bar = Bar {};
let xs: Vec<&dyn Signer> = vec![&foo, &bar];
assert_eq!(
xs.sign_message(b""),
vec![Signature::default(), Signature::default()],
);
// Same as above, but less compiler magic.
let xs_ref: &[&dyn Signer] = &xs;
assert_eq!(
Signers::sign_message(xs_ref, b""),
vec![Signature::default(), Signature::default()],
);
}
} }

View File

@ -379,7 +379,7 @@ mod tests {
use crate::{ use crate::{
hash::hash, hash::hash,
instruction::AccountMeta, instruction::AccountMeta,
signature::{Keypair, Signer}, signature::{Keypair, Presigner, Signer},
system_instruction, system_instruction,
}; };
use bincode::{deserialize, serialize, serialized_size}; use bincode::{deserialize, serialize, serialized_size};
@ -671,4 +671,52 @@ mod tests {
); );
assert!(tx.is_signed()); assert!(tx.is_signed());
} }
#[test]
fn test_try_sign_dyn_keypairs() {
let program_id = Pubkey::default();
let keypair = Keypair::new();
let pubkey = keypair.pubkey();
let presigner_keypair = Keypair::new();
let presigner_pubkey = presigner_keypair.pubkey();
let ix = Instruction::new(
program_id,
&0,
vec![
AccountMeta::new(pubkey, true),
AccountMeta::new(presigner_pubkey, true),
],
);
let mut tx = Transaction::new_unsigned_instructions(vec![ix]);
let presigner_sig = presigner_keypair.sign_message(&tx.message_data());
let presigner = Presigner::new(&presigner_pubkey, &presigner_sig);
let signers: Vec<&dyn Signer> = vec![&keypair, &presigner];
let res = tx.try_sign(&signers, Hash::default());
assert_eq!(res, Ok(()));
assert_eq!(tx.signatures[0], keypair.sign_message(&tx.message_data()));
assert_eq!(tx.signatures[1], presigner_sig);
// Wrong key should error, not panic
let another_pubkey = Pubkey::new_rand();
let ix = Instruction::new(
program_id,
&0,
vec![
AccountMeta::new(another_pubkey, true),
AccountMeta::new(presigner_pubkey, true),
],
);
let mut tx = Transaction::new_unsigned_instructions(vec![ix]);
let res = tx.try_sign(&signers, Hash::default());
assert!(res.is_err());
assert_eq!(
tx.signatures,
vec![Signature::default(), Signature::default()]
);
}
} }