From 1a4de4d3c4a2203a47eca0cb106d7403a46300f6 Mon Sep 17 00:00:00 2001 From: Tyera Eulberg Date: Wed, 26 Feb 2020 17:59:41 -0700 Subject: [PATCH] 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 * 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 * Add flag to confirm key on device Co-authored-by: Trent Nelson Co-authored-by: Greg Fitzgerald --- Cargo.lock | 1 + book/src/SUMMARY.md | 2 + book/src/paper-wallet/usage.md | 8 +- book/src/remote-wallet/README.md | 12 + book/src/remote-wallet/ledger.md | 132 +++ clap-utils/src/input_parsers.rs | 34 +- clap-utils/src/input_validators.rs | 18 +- clap-utils/src/keypair.rs | 89 +- clap-utils/src/lib.rs | 1 + clap-utils/src/offline.rs | 19 + cli/src/cli.rs | 1259 ++++++++++++++------------- cli/src/cluster_query.rs | 89 +- cli/src/main.rs | 105 +-- cli/src/nonce.rs | 311 ++++--- cli/src/offline.rs | 20 +- cli/src/stake.rs | 1226 +++++++++++++------------- cli/src/storage.rs | 110 ++- cli/src/validator_info.rs | 46 +- cli/src/vote.rs | 178 ++-- cli/tests/deploy.rs | 4 +- cli/tests/nonce.rs | 173 ++-- cli/tests/pay.rs | 115 +-- cli/tests/request_airdrop.rs | 9 +- cli/tests/stake.rs | 713 +++++++-------- cli/tests/transfer.rs | 82 +- keygen/src/keygen.rs | 38 +- remote-wallet/Cargo.toml | 1 + remote-wallet/src/ledger.rs | 46 +- remote-wallet/src/remote_keypair.rs | 36 +- remote-wallet/src/remote_wallet.rs | 252 ++++-- sdk/src/signature.rs | 26 +- sdk/src/signers.rs | 46 + sdk/src/transaction.rs | 50 +- 33 files changed, 2895 insertions(+), 2356 deletions(-) create mode 100644 book/src/remote-wallet/README.md create mode 100644 book/src/remote-wallet/ledger.md create mode 100644 clap-utils/src/offline.rs diff --git a/Cargo.lock b/Cargo.lock index 4999ca0c27..19be2f3611 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4319,6 +4319,7 @@ dependencies = [ "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "solana-sdk 0.23.7", "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]] diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 8d4dda51e8..14a466945d 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -3,6 +3,8 @@ * [Introduction](introduction.md) * [Using Solana from the Command-line](cli/README.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) * [Installation](paper-wallet/installation.md) * [Paper Wallet Usage](paper-wallet/usage.md) diff --git a/book/src/paper-wallet/usage.md b/book/src/paper-wallet/usage.md index 0010eeb831..a404ad88e6 100644 --- a/book/src/paper-wallet/usage.md +++ b/book/src/paper-wallet/usage.md @@ -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, 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 -`--ask-seed-phrase keypair` to securely input the funding keypair. +`--keypair ASK` to securely input the funding keypair. ```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] 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: ``` -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. ```bash -solana delegate-stake --ask-seed-phrase keypair +solana delegate-stake --keypair ASK [keypair] seed phrase: 🔒 [keypair] If this seed phrase has an associated passphrase, enter it now. Otherwise, press ENTER to continue: diff --git a/book/src/remote-wallet/README.md b/book/src/remote-wallet/README.md new file mode 100644 index 0000000000..25babad4d8 --- /dev/null +++ b/book/src/remote-wallet/README.md @@ -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" %} diff --git a/book/src/remote-wallet/ledger.md b/book/src/remote-wallet/ledger.md new file mode 100644 index 0000000000..aaf897cceb --- /dev/null +++ b/book/src/remote-wallet/ledger.md @@ -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/ +``` + +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[/[/]][?key=] +``` + +`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 `[/]`, 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 +``` + +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 --from +``` + +Or with the default signer: + +```text +solana transfer +``` + +### Delegate Stake with Ledger Device + +```text +solana delegate-stake --keypair +``` + +Or with the default signer: + +```text +solana delegate-stake +``` + +## Support + +Email maintainers@solana.com diff --git a/clap-utils/src/input_parsers.rs b/clap-utils/src/input_parsers.rs index a2b2c333c3..9504b79866 100644 --- a/clap-utils/src/input_parsers.rs +++ b/clap-utils/src/input_parsers.rs @@ -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 clap::ArgMatches; -use solana_remote_wallet::remote_wallet::DerivationPath; +use solana_remote_wallet::remote_wallet::{DerivationPath, RemoteWalletManager}; use solana_sdk::{ clock::UnixTimestamp, native_token::sol_to_lamports, pubkey::Pubkey, signature::{read_keypair_file, Keypair, Signature, Signer}, }; -use std::str::FromStr; +use std::{str::FromStr, sync::Arc}; // Return parsed values from matches at `name` pub fn values_of(matches: &ArgMatches<'_>, name: &str) -> Option> @@ -93,6 +95,22 @@ pub fn pubkeys_sigs_of(matches: &ArgMatches<'_>, name: &str) -> Option, + name: &str, + wallet_manager: Option<&Arc>, +) -> Result<(Option>, Option), Box> { + 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 { value_of(matches, name).map(sol_to_lamports) } @@ -101,8 +119,8 @@ pub fn derivation_of(matches: &ArgMatches<'_>, name: &str) -> Option().unwrap(); - let change = parts.next().map(|change| change.parse::().unwrap()); + let account = parts.next().map(|account| account.parse::().unwrap()); + let change = parts.next().map(|change| change.parse::().unwrap()); DerivationPath { account, change } }) } @@ -290,7 +308,7 @@ mod tests { assert_eq!( derivation_of(&matches, "single"), Some(DerivationPath { - account: 2, + account: Some(2), change: Some(3) }) ); @@ -301,7 +319,7 @@ mod tests { assert_eq!( derivation_of(&matches, "single"), Some(DerivationPath { - account: 2, + account: Some(2), change: None }) ); @@ -312,7 +330,7 @@ mod tests { assert_eq!( derivation_of(&matches, "single"), Some(DerivationPath { - account: 2, + account: Some(2), change: Some(3) }) ); diff --git a/clap-utils/src/input_validators.rs b/clap-utils/src/input_validators.rs index 6c68278418..0419abc058 100644 --- a/clap-utils/src/input_validators.rs +++ b/clap-utils/src/input_validators.rs @@ -1,4 +1,4 @@ -use crate::keypair::ASK_KEYWORD; +use crate::keypair::{parse_keypair_path, KeypairUrl, ASK_KEYWORD}; use chrono::DateTime; use solana_sdk::{ 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)) } +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 pub fn is_pubkey_sig(string: String) -> Result<(), String> { let mut signer = string.split('='); @@ -135,7 +142,7 @@ pub fn is_derivation(value: String) -> Result<(), String> { let mut parts = value.split('/'); let account = parts.next().unwrap(); account - .parse::() + .parse::() .map_err(|e| { format!( "Unable to parse derivation, provided: {}, err: {:?}", @@ -144,7 +151,7 @@ pub fn is_derivation(value: String) -> Result<(), String> { }) .and_then(|_| { if let Some(change) = parts.next() { - change.parse::().map_err(|e| { + change.parse::().map_err(|e| { format!( "Unable to parse derivation, provided: {}, err: {:?}", change, e @@ -165,11 +172,12 @@ mod tests { fn test_is_derivation() { assert_eq!(is_derivation("2".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!(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("0/65537".to_string()).is_err()); + assert!(is_derivation("0/4294967296".to_string()).is_err()); } } diff --git a/clap-utils/src/keypair.rs b/clap-utils/src/keypair.rs index d8be15ae17..2b625ca9a1 100644 --- a/clap-utils/src/keypair.rs +++ b/clap-utils/src/keypair.rs @@ -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 clap::values_t; +use clap::{values_t, ArgMatches, Error, ErrorKind}; use rpassword::prompt_password_stderr; -use solana_sdk::signature::{ - keypair_from_seed, keypair_from_seed_phrase_and_passphrase, read_keypair_file, Keypair, Signer, +use solana_remote_wallet::{ + 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::{ error, io::{stdin, stdout, Write}, process::exit, + str::FromStr, + sync::Arc, }; pub enum KeypairUrl { @@ -16,6 +30,7 @@ pub enum KeypairUrl { Filepath(String), Usb(String), Stdin, + Pubkey(Pubkey), } 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 { KeypairUrl::Ask } 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 { KeypairUrl::Filepath(path.to_string()) } } +pub fn presigner_from_pubkey_sigs( + pubkey: &Pubkey, + signers: &[(Pubkey, Signature)], +) -> Option { + 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>, +) -> Result, Box> { + 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 pub const ASK_KEYWORD: &str = "ASK"; diff --git a/clap-utils/src/lib.rs b/clap-utils/src/lib.rs index ea5bd45d46..a66efa3981 100644 --- a/clap-utils/src/lib.rs +++ b/clap-utils/src/lib.rs @@ -26,3 +26,4 @@ pub struct ArgConstant<'a> { pub mod input_parsers; pub mod input_validators; pub mod keypair; +pub mod offline; diff --git a/clap-utils/src/offline.rs b/clap-utils/src/offline.rs new file mode 100644 index 0000000000..b0533a28de --- /dev/null +++ b/clap-utils/src/offline.rs @@ -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", +}; diff --git a/cli/src/cli.rs b/cli/src/cli.rs index b5cdd4e442..24510a1111 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -14,16 +14,16 @@ use log::*; use num_traits::FromPrimitive; use serde_json::{self, json, Value}; use solana_budget_program::budget_instruction::{self, BudgetError}; -use solana_clap_utils::{input_parsers::*, input_validators::*, ArgConstant}; +use solana_clap_utils::{ + input_parsers::*, input_validators::*, keypair::signer_from_path, offline::SIGN_ONLY_ARG, + ArgConstant, +}; use solana_client::{client_error::ClientError, rpc_client::RpcClient}; #[cfg(not(test))] use solana_faucet::faucet::request_airdrop_transaction; #[cfg(test)] use solana_faucet::faucet_mock::request_airdrop_transaction; -use solana_remote_wallet::{ - ledger::get_ledger_from_info, - remote_wallet::{DerivationPath, RemoteWallet, RemoteWalletInfo}, -}; +use solana_remote_wallet::remote_wallet::{DerivationPath, RemoteWalletManager}; use solana_sdk::{ bpf_loader, clock::{Epoch, Slot}, @@ -36,9 +36,8 @@ use solana_sdk::{ message::Message, native_token::lamports_to_sol, pubkey::Pubkey, - signature::{keypair_from_seed, Keypair, Signature, Signer, SignerError}, + signature::{Keypair, Signature, Signer, SignerError}, system_instruction::{self, create_address_with_seed, SystemError, MAX_ADDRESS_SEED_LEN}, - system_transaction, transaction::{Transaction, TransactionError}, }; use solana_stake_program::stake_state::{Lockup, StakeAuthorize}; @@ -48,12 +47,58 @@ use std::{ fs::File, io::{Read, Write}, net::{IpAddr, SocketAddr}, + sync::Arc, thread::sleep, time::Duration, {error, fmt}, }; -const USERDATA_CHUNK_SIZE: usize = 229; // Keep program chunks under PACKET_DATA_SIZE +pub type CliSigners = Vec>; +pub type SignerIndex = usize; +pub(crate) struct CliSignerInfo { + pub signers: CliSigners, +} + +impl CliSignerInfo { + pub(crate) fn index_of(&self, pubkey: Option) -> Option { + if let Some(pubkey) = pubkey { + self.signers + .iter() + .position(|signer| signer.pubkey() == pubkey) + } else { + Some(0) + } + } +} + +pub(crate) fn generate_unique_signers( + bulk_signers: Vec>>, + matches: &ArgMatches<'_>, + default_signer_path: &str, + wallet_manager: Option<&Arc>, +) -> Result> { + let mut unique_signers = vec![]; + + // Determine if the default signer is needed + if bulk_signers.iter().any(|signer| signer.is_none()) { + let default_signer = + signer_from_path(matches, default_signer_path, "keypair", wallet_manager)?; + unique_signers.push(default_signer); + } + + for signer in bulk_signers.into_iter() { + if let Some(signer) = signer { + if !unique_signers.iter().any(|s| s == &signer) { + unique_signers.push(signer); + } + } + } + Ok(CliSignerInfo { + signers: unique_signers, + }) +} + +const DATA_CHUNK_SIZE: usize = 229; // Keep program chunks under PACKET_DATA_SIZE pub const FEE_PAYER_ARG: ArgConstant<'static> = ArgConstant { name: "fee_payer", @@ -67,8 +112,8 @@ pub fn fee_payer_arg<'a, 'b>() -> Arg<'a, 'b> { Arg::with_name(FEE_PAYER_ARG.name) .long(FEE_PAYER_ARG.long) .takes_value(true) - .value_name("KEYPAIR or PUBKEY") - .validator(is_pubkey_or_keypair_or_ask_keyword) + .value_name("KEYPAIR or PUBKEY or REMOTE WALLET PATH") + .validator(is_valid_signer) .help(FEE_PAYER_ARG.help) } @@ -94,85 +139,6 @@ impl std::ops::Deref for KeypairEq { } } -#[derive(Debug)] -pub enum SigningAuthority { - Online(Keypair), - // We hold a random keypair alongside our legit pubkey in order - // to generate a placeholder signature in the transaction - Offline(Pubkey, Keypair), -} - -impl SigningAuthority { - pub fn new_from_matches( - matches: &ArgMatches<'_>, - name: &str, - signers: Option<&[(Pubkey, Signature)]>, - ) -> Result, CliError> { - if matches.is_present(name) { - keypair_of(matches, name) - .map(|keypair| keypair.into()) - .or_else(|| { - pubkey_of(matches, name) - .filter(|pubkey| { - signers - .and_then(|signers| { - signers.iter().find(|(signer, _sig)| *signer == *pubkey) - }) - .is_some() - }) - .map(|pubkey| pubkey.into()) - }) - .ok_or_else(|| CliError::BadParameter("Invalid authority".to_string())) - .map(Some) - } else { - Ok(None) - } - } - - pub fn keypair(&self) -> &Keypair { - match self { - SigningAuthority::Online(keypair) => keypair, - SigningAuthority::Offline(_pubkey, keypair) => keypair, - } - } - - pub fn pubkey(&self) -> Pubkey { - match self { - SigningAuthority::Online(keypair) => keypair.pubkey(), - SigningAuthority::Offline(pubkey, _keypair) => *pubkey, - } - } -} - -impl From for SigningAuthority { - fn from(keypair: Keypair) -> Self { - SigningAuthority::Online(keypair) - } -} - -impl From for SigningAuthority { - fn from(pubkey: Pubkey) -> Self { - SigningAuthority::Offline(pubkey, keypair_from_seed(pubkey.as_ref()).unwrap()) - } -} - -impl PartialEq for SigningAuthority { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (SigningAuthority::Online(keypair1), SigningAuthority::Online(keypair2)) => { - keypair1.pubkey() == keypair2.pubkey() - } - (SigningAuthority::Online(keypair), SigningAuthority::Offline(pubkey, _)) - | (SigningAuthority::Offline(pubkey, _), SigningAuthority::Online(keypair)) => { - keypair.pubkey() == *pubkey - } - (SigningAuthority::Offline(pubkey1, _), SigningAuthority::Offline(pubkey2, _)) => { - pubkey1 == pubkey2 - } - } - } -} - pub fn nonce_authority_arg<'a, 'b>() -> Arg<'a, 'b> { nonce::nonce_authority_arg().requires(NONCE_ARG.name) } @@ -186,10 +152,9 @@ pub struct PayCommand { pub witnesses: Option>, pub cancelable: bool, pub sign_only: bool, - pub signers: Option>, pub blockhash_query: BlockhashQuery, pub nonce_account: Option, - pub nonce_authority: Option, + pub nonce_authority: SignerIndex, } #[derive(Debug, PartialEq)] @@ -242,11 +207,11 @@ pub enum CliCommand { // Nonce commands AuthorizeNonceAccount { nonce_account: Pubkey, - nonce_authority: Option, + nonce_authority: SignerIndex, new_authority: Pubkey, }, CreateNonceAccount { - nonce_account: KeypairEq, + nonce_account: SignerIndex, seed: Option, nonce_authority: Option, lamports: u64, @@ -254,7 +219,7 @@ pub enum CliCommand { GetNonce(Pubkey), NewNonce { nonce_account: Pubkey, - nonce_authority: Option, + nonce_authority: SignerIndex, }, ShowNonceAccount { nonce_account_pubkey: Pubkey, @@ -262,7 +227,7 @@ pub enum CliCommand { }, WithdrawFromNonceAccount { nonce_account: Pubkey, - nonce_authority: Option, + nonce_authority: SignerIndex, destination_account_pubkey: Pubkey, lamports: u64, }, @@ -270,54 +235,50 @@ pub enum CliCommand { Deploy(String), // Stake Commands CreateStakeAccount { - stake_account: SigningAuthority, + stake_account: SignerIndex, seed: Option, staker: Option, withdrawer: Option, lockup: Lockup, lamports: u64, sign_only: bool, - signers: Option>, blockhash_query: BlockhashQuery, nonce_account: Option, - nonce_authority: Option, - fee_payer: Option, - from: Option, + nonce_authority: SignerIndex, + fee_payer: SignerIndex, + from: SignerIndex, }, DeactivateStake { stake_account_pubkey: Pubkey, - stake_authority: Option, + stake_authority: SignerIndex, sign_only: bool, - signers: Option>, blockhash_query: BlockhashQuery, nonce_account: Option, - nonce_authority: Option, - fee_payer: Option, + nonce_authority: SignerIndex, + fee_payer: SignerIndex, }, DelegateStake { stake_account_pubkey: Pubkey, vote_account_pubkey: Pubkey, - stake_authority: Option, + stake_authority: SignerIndex, force: bool, sign_only: bool, - signers: Option>, blockhash_query: BlockhashQuery, nonce_account: Option, - nonce_authority: Option, - fee_payer: Option, + nonce_authority: SignerIndex, + fee_payer: SignerIndex, }, SplitStake { stake_account_pubkey: Pubkey, - stake_authority: Option, + stake_authority: SignerIndex, sign_only: bool, - signers: Option>, blockhash_query: BlockhashQuery, nonce_account: Option, - nonce_authority: Option, - split_stake_account: KeypairEq, + nonce_authority: SignerIndex, + split_stake_account: SignerIndex, seed: Option, lamports: u64, - fee_payer: Option, + fee_payer: SignerIndex, }, ShowStakeHistory { use_lamports_unit: bool, @@ -330,41 +291,38 @@ pub enum CliCommand { stake_account_pubkey: Pubkey, new_authorized_pubkey: Pubkey, stake_authorize: StakeAuthorize, - authority: Option, + authority: SignerIndex, sign_only: bool, - signers: Option>, blockhash_query: BlockhashQuery, nonce_account: Option, - nonce_authority: Option, - fee_payer: Option, + nonce_authority: SignerIndex, + fee_payer: SignerIndex, }, StakeSetLockup { stake_account_pubkey: Pubkey, lockup: Lockup, - custodian: Option, + custodian: SignerIndex, sign_only: bool, - signers: Option>, blockhash_query: BlockhashQuery, nonce_account: Option, - nonce_authority: Option, - fee_payer: Option, + nonce_authority: SignerIndex, + fee_payer: SignerIndex, }, WithdrawStake { stake_account_pubkey: Pubkey, destination_account_pubkey: Pubkey, lamports: u64, - withdraw_authority: Option, + withdraw_authority: SignerIndex, sign_only: bool, - signers: Option>, blockhash_query: BlockhashQuery, nonce_account: Option, - nonce_authority: Option, - fee_payer: Option, + nonce_authority: SignerIndex, + fee_payer: SignerIndex, }, // Storage Commands CreateStorageAccount { account_owner: Pubkey, - storage_account: KeypairEq, + storage_account: SignerIndex, account_type: StorageAccountType, }, ClaimStorageReward { @@ -381,7 +339,6 @@ pub enum CliCommand { }, // Vote Commands CreateVoteAccount { - vote_account: KeypairEq, seed: Option, node_pubkey: Pubkey, authorized_voter: Option, @@ -400,7 +357,6 @@ pub enum CliCommand { VoteUpdateValidator { vote_account_pubkey: Pubkey, new_identity_pubkey: Pubkey, - authorized_voter: KeypairEq, }, // Wallet Commands Address, @@ -426,13 +382,12 @@ pub enum CliCommand { Transfer { lamports: u64, to: Pubkey, - from: Option, + from: SignerIndex, sign_only: bool, - signers: Option>, blockhash_query: BlockhashQuery, nonce_account: Option, - nonce_authority: Option, - fee_payer: Option, + nonce_authority: SignerIndex, + fee_payer: SignerIndex, }, Witness(Pubkey, Pubkey), // Witness(to, process_id) } @@ -440,7 +395,7 @@ pub enum CliCommand { #[derive(Debug, PartialEq)] pub struct CliCommandInfo { pub command: CliCommand, - pub require_keypair: bool, + pub signers: CliSigners, } #[derive(Debug, Clone, PartialEq)] @@ -471,17 +426,23 @@ impl error::Error for CliError { } } -pub struct CliConfig { +impl From> for CliError { + fn from(error: Box) -> Self { + CliError::DynamicProgramError(format!("{:?}", error)) + } +} + +pub struct CliConfig<'a> { pub command: CliCommand, pub json_rpc_url: String, - pub keypair: Keypair, - pub keypair_path: Option, + pub signers: Vec<&'a dyn Signer>, + pub keypair_path: String, pub derivation_path: Option, pub rpc_client: Option, pub verbose: bool, } -impl CliConfig { +impl CliConfig<'_> { pub fn default_keypair_path() -> String { let mut keypair_path = dirs::home_dir().expect("home directory"); keypair_path.extend(&[".config", "solana", "id.json"]); @@ -492,33 +453,27 @@ impl CliConfig { "http://127.0.0.1:8899".to_string() } - pub(crate) fn pubkey(&self) -> Result> { - if let Some(path) = &self.keypair_path { - if path.starts_with("usb://") { - let (remote_wallet_info, mut derivation_path) = - RemoteWalletInfo::parse_path(path.to_string())?; - if let Some(derivation) = &self.derivation_path { - let derivation = derivation.clone(); - derivation_path = derivation; - } - let ledger = get_ledger_from_info(remote_wallet_info)?; - return Ok(ledger.get_pubkey(&derivation_path)?); - } + pub(crate) fn pubkey(&self) -> Result { + if !self.signers.is_empty() { + self.signers[0].try_pubkey() + } else { + Err(SignerError::CustomError( + "Default keypair must be set if pubkey arg not provided".to_string(), + )) } - Ok(self.keypair.pubkey()) } } -impl Default for CliConfig { - fn default() -> CliConfig { +impl Default for CliConfig<'_> { + fn default() -> CliConfig<'static> { CliConfig { command: CliCommand::Balance { pubkey: Some(Pubkey::default()), use_lamports_unit: false, }, json_rpc_url: Self::default_json_rpc_url(), - keypair: Keypair::new(), - keypair_path: Some(Self::default_keypair_path()), + signers: Vec::new(), + keypair_path: Self::default_keypair_path(), derivation_path: None, rpc_client: None, verbose: false, @@ -526,97 +481,149 @@ impl Default for CliConfig { } } -pub fn parse_command(matches: &ArgMatches<'_>) -> Result> { +pub fn parse_command( + matches: &ArgMatches<'_>, + default_signer_path: &str, + wallet_manager: Option<&Arc>, +) -> Result> { let response = match matches.subcommand() { // Cluster Query Commands ("catchup", Some(matches)) => parse_catchup(matches), ("cluster-version", Some(_matches)) => Ok(CliCommandInfo { command: CliCommand::ClusterVersion, - require_keypair: false, + signers: vec![], }), ("create-address-with-seed", Some(matches)) => parse_create_address_with_seed(matches), ("fees", Some(_matches)) => Ok(CliCommandInfo { command: CliCommand::Fees, - require_keypair: false, + signers: vec![], }), ("block-time", Some(matches)) => parse_get_block_time(matches), ("epoch-info", Some(matches)) => parse_get_epoch_info(matches), ("genesis-hash", Some(_matches)) => Ok(CliCommandInfo { command: CliCommand::GetGenesisHash, - require_keypair: false, + signers: vec![], }), ("slot", Some(matches)) => parse_get_slot(matches), ("transaction-count", Some(matches)) => parse_get_transaction_count(matches), ("leader-schedule", Some(_matches)) => Ok(CliCommandInfo { command: CliCommand::LeaderSchedule, - require_keypair: false, + signers: vec![], }), - ("ping", Some(matches)) => parse_cluster_ping(matches), + ("ping", Some(matches)) => parse_cluster_ping(matches, default_signer_path, wallet_manager), ("block-production", Some(matches)) => parse_show_block_production(matches), ("gossip", Some(_matches)) => Ok(CliCommandInfo { command: CliCommand::ShowGossip, - require_keypair: false, + signers: vec![], }), ("stakes", Some(matches)) => parse_show_stakes(matches), ("validators", Some(matches)) => parse_show_validators(matches), // Nonce Commands - ("authorize-nonce-account", Some(matches)) => parse_authorize_nonce_account(matches), - ("create-nonce-account", Some(matches)) => parse_nonce_create_account(matches), + ("authorize-nonce-account", Some(matches)) => { + parse_authorize_nonce_account(matches, default_signer_path, wallet_manager) + } + ("create-nonce-account", Some(matches)) => { + parse_nonce_create_account(matches, default_signer_path, wallet_manager) + } ("nonce", Some(matches)) => parse_get_nonce(matches), - ("new-nonce", Some(matches)) => parse_new_nonce(matches), + ("new-nonce", Some(matches)) => { + parse_new_nonce(matches, default_signer_path, wallet_manager) + } ("nonce-account", Some(matches)) => parse_show_nonce_account(matches), ("withdraw-from-nonce-account", Some(matches)) => { - parse_withdraw_from_nonce_account(matches) + parse_withdraw_from_nonce_account(matches, default_signer_path, wallet_manager) } // Program Deployment ("deploy", Some(matches)) => Ok(CliCommandInfo { command: CliCommand::Deploy(matches.value_of("program_location").unwrap().to_string()), - require_keypair: true, + signers: vec![signer_from_path( + matches, + default_signer_path, + "keypair", + wallet_manager, + )?], }), // Stake Commands - ("create-stake-account", Some(matches)) => parse_stake_create_account(matches), - ("delegate-stake", Some(matches)) => parse_stake_delegate_stake(matches), - ("withdraw-stake", Some(matches)) => parse_stake_withdraw_stake(matches), - ("deactivate-stake", Some(matches)) => parse_stake_deactivate_stake(matches), - ("split-stake", Some(matches)) => parse_split_stake(matches), - ("stake-authorize-staker", Some(matches)) => { - parse_stake_authorize(matches, StakeAuthorize::Staker) + ("create-stake-account", Some(matches)) => { + parse_stake_create_account(matches, default_signer_path, wallet_manager) } - ("stake-authorize-withdrawer", Some(matches)) => { - parse_stake_authorize(matches, StakeAuthorize::Withdrawer) + ("delegate-stake", Some(matches)) => { + parse_stake_delegate_stake(matches, default_signer_path, wallet_manager) + } + ("withdraw-stake", Some(matches)) => { + parse_stake_withdraw_stake(matches, default_signer_path, wallet_manager) + } + ("deactivate-stake", Some(matches)) => { + parse_stake_deactivate_stake(matches, default_signer_path, wallet_manager) + } + ("split-stake", Some(matches)) => { + parse_split_stake(matches, default_signer_path, wallet_manager) + } + ("stake-authorize-staker", Some(matches)) => parse_stake_authorize( + matches, + default_signer_path, + wallet_manager, + StakeAuthorize::Staker, + ), + ("stake-authorize-withdrawer", Some(matches)) => parse_stake_authorize( + matches, + default_signer_path, + wallet_manager, + StakeAuthorize::Withdrawer, + ), + ("stake-set-lockup", Some(matches)) => { + parse_stake_set_lockup(matches, default_signer_path, wallet_manager) } - ("stake-set-lockup", Some(matches)) => parse_stake_set_lockup(matches), ("stake-account", Some(matches)) => parse_show_stake_account(matches), ("stake-history", Some(matches)) => parse_show_stake_history(matches), // Storage Commands ("create-archiver-storage-account", Some(matches)) => { - parse_storage_create_archiver_account(matches) + parse_storage_create_archiver_account(matches, default_signer_path, wallet_manager) } ("create-validator-storage-account", Some(matches)) => { - parse_storage_create_validator_account(matches) + parse_storage_create_validator_account(matches, default_signer_path, wallet_manager) + } + ("claim-storage-reward", Some(matches)) => { + parse_storage_claim_reward(matches, default_signer_path, wallet_manager) } - ("claim-storage-reward", Some(matches)) => parse_storage_claim_reward(matches), ("storage-account", Some(matches)) => parse_storage_get_account_command(matches), // Validator Info Commands ("validator-info", Some(matches)) => match matches.subcommand() { - ("publish", Some(matches)) => parse_validator_info_command(matches), + ("publish", Some(matches)) => { + parse_validator_info_command(matches, default_signer_path, wallet_manager) + } ("get", Some(matches)) => parse_get_validator_info_command(matches), _ => unreachable!(), }, // Vote Commands - ("create-vote-account", Some(matches)) => parse_vote_create_account(matches), - ("vote-update-validator", Some(matches)) => parse_vote_update_validator(matches), - ("vote-authorize-voter", Some(matches)) => { - parse_vote_authorize(matches, VoteAuthorize::Voter) + ("create-vote-account", Some(matches)) => { + parse_vote_create_account(matches, default_signer_path, wallet_manager) } - ("vote-authorize-withdrawer", Some(matches)) => { - parse_vote_authorize(matches, VoteAuthorize::Withdrawer) + ("vote-update-validator", Some(matches)) => { + parse_vote_update_validator(matches, default_signer_path, wallet_manager) } + ("vote-authorize-voter", Some(matches)) => parse_vote_authorize( + matches, + default_signer_path, + wallet_manager, + VoteAuthorize::Voter, + ), + ("vote-authorize-withdrawer", Some(matches)) => parse_vote_authorize( + matches, + default_signer_path, + wallet_manager, + VoteAuthorize::Withdrawer, + ), ("vote-account", Some(matches)) => parse_vote_get_account_command(matches), // Wallet Commands - ("address", Some(_matches)) => Ok(CliCommandInfo { + ("address", Some(matches)) => Ok(CliCommandInfo { command: CliCommand::Address, - require_keypair: true, + signers: vec![signer_from_path( + matches, + default_signer_path, + "keypair", + wallet_manager, + )?], }), ("airdrop", Some(matches)) => { let faucet_port = matches @@ -640,7 +647,17 @@ pub fn parse_command(matches: &ArgMatches<'_>) -> Result) -> Result { - let pubkey = pubkey_of(&matches, "pubkey"); + let pubkey = pubkey_of(matches, "pubkey"); + let signers = if pubkey.is_some() { + vec![] + } else { + vec![signer_from_path( + matches, + default_signer_path, + "keypair", + wallet_manager, + )?] + }; Ok(CliCommandInfo { command: CliCommand::Balance { pubkey, use_lamports_unit: matches.is_present("lamports"), }, - require_keypair: pubkey.is_none(), + signers, }) } ("cancel", Some(matches)) => { let process_id = value_of(matches, "process_id").unwrap(); + let default_signer = + signer_from_path(matches, default_signer_path, "keypair", wallet_manager)?; + Ok(CliCommandInfo { command: CliCommand::Cancel(process_id), - require_keypair: true, + signers: vec![default_signer], }) } ("confirm", Some(matches)) => match matches.value_of("signature").unwrap().parse() { Ok(signature) => Ok(CliCommandInfo { command: CliCommand::Confirm(signature), - require_keypair: false, + signers: vec![], }), _ => { eprintln!("{}", matches.usage()); @@ -681,7 +711,7 @@ pub fn parse_command(matches: &ArgMatches<'_>) -> Result { let lamports = lamports_of_sol(matches, "amount").unwrap(); - let to = pubkey_of(&matches, "to").unwrap(); + let to = pubkey_of(matches, "to").unwrap(); let timestamp = if matches.is_present("timestamp") { // Parse input for serde_json let date_string = if !matches.value_of("timestamp").unwrap().contains('Z') { @@ -693,17 +723,25 @@ pub fn parse_command(matches: &ArgMatches<'_>) -> Result) -> Result { @@ -733,20 +770,24 @@ pub fn parse_command(matches: &ArgMatches<'_>) -> Result { - let to = value_of(&matches, "to").unwrap(); - let process_id = value_of(&matches, "process_id").unwrap(); + let to = value_of(matches, "to").unwrap(); + let process_id = value_of(matches, "process_id").unwrap(); + + let default_signer = + signer_from_path(matches, default_signer_path, "keypair", wallet_manager)?; + Ok(CliCommandInfo { command: CliCommand::Witness(to, process_id), - require_keypair: true, + signers: vec![default_signer], }) } ("send-timestamp", Some(matches)) => { - let to = value_of(&matches, "to").unwrap(); - let process_id = value_of(&matches, "process_id").unwrap(); + let to = value_of(matches, "to").unwrap(); + let process_id = value_of(matches, "process_id").unwrap(); let dt = if matches.is_present("datetime") { // Parse input for serde_json let date_string = if !matches.value_of("datetime").unwrap().contains('Z') { @@ -758,42 +799,50 @@ pub fn parse_command(matches: &ArgMatches<'_>) -> Result { let lamports = lamports_of_sol(matches, "amount").unwrap(); - let to = pubkey_of(&matches, "to").unwrap(); + let to = pubkey_of(matches, "to").unwrap(); let sign_only = matches.is_present(SIGN_ONLY_ARG.name); - let signers = pubkeys_sigs_of(&matches, SIGNER_ARG.name); let blockhash_query = BlockhashQuery::new_from_matches(matches); - let nonce_account = pubkey_of(&matches, NONCE_ARG.name); - let nonce_authority = SigningAuthority::new_from_matches( - &matches, - NONCE_AUTHORITY_ARG.name, - signers.as_deref(), + let nonce_account = pubkey_of(matches, NONCE_ARG.name); + let (nonce_authority, nonce_authority_pubkey) = + signer_of(matches, NONCE_AUTHORITY_ARG.name, wallet_manager)?; + let (fee_payer, fee_payer_pubkey) = + signer_of(matches, FEE_PAYER_ARG.name, wallet_manager)?; + let (from, from_pubkey) = signer_of(matches, "from", wallet_manager)?; + + let mut bulk_signers = vec![fee_payer, from]; + if nonce_account.is_some() { + bulk_signers.push(nonce_authority); + } + + let signer_info = generate_unique_signers( + bulk_signers, + matches, + default_signer_path, + wallet_manager, )?; - let fee_payer = SigningAuthority::new_from_matches( - &matches, - FEE_PAYER_ARG.name, - signers.as_deref(), - )?; - let from = SigningAuthority::new_from_matches(&matches, "from", signers.as_deref())?; + Ok(CliCommandInfo { command: CliCommand::Transfer { lamports, to, - from, sign_only, - signers, blockhash_query, nonce_account, - nonce_authority, - fee_payer, + nonce_authority: signer_info.index_of(nonce_authority_pubkey).unwrap(), + fee_payer: signer_info.index_of(fee_payer_pubkey).unwrap(), + from: signer_info.index_of(from_pubkey).unwrap(), }, - require_keypair: true, + signers: signer_info.signers, }) } // @@ -884,23 +933,11 @@ pub fn return_signers(tx: &Transaction) -> ProcessResult { .to_string()) } -pub fn replace_signatures(tx: &mut Transaction, signers: &[(Pubkey, Signature)]) -> ProcessResult { - tx.replace_signatures(signers).map_err(|_| { - CliError::BadParameter( - "Transaction construction failed, incorrect signature or public key provided" - .to_string(), - ) - })?; - Ok("".to_string()) -} - pub fn parse_create_address_with_seed( matches: &ArgMatches<'_>, ) -> Result { let from_pubkey = pubkey_of(matches, "from"); - let require_keypair = from_pubkey.is_none(); - let program_id = match matches.value_of("program_id").unwrap() { "STAKE" => solana_stake_program::id(), "VOTE" => solana_vote_program::id(), @@ -922,7 +959,7 @@ pub fn parse_create_address_with_seed( seed, program_id, }, - require_keypair, + signers: vec![], }) } @@ -945,7 +982,11 @@ fn process_airdrop( pubkey: &Option, lamports: u64, ) -> ProcessResult { - let pubkey = pubkey.unwrap_or(config.pubkey()?); + let pubkey = if let Some(pubkey) = pubkey { + *pubkey + } else { + config.pubkey()? + }; println!( "Requesting airdrop of {} from {}", build_balance_message(lamports, false, true), @@ -976,7 +1017,11 @@ fn process_balance( pubkey: &Option, use_lamports_unit: bool, ) -> ProcessResult { - let pubkey = pubkey.unwrap_or(config.pubkey()?); + let pubkey = if let Some(pubkey) = pubkey { + *pubkey + } else { + config.pubkey()? + }; let balance = rpc_client.retry_get_balance(&pubkey, 5)?; match balance { Some(lamports) => Ok(build_balance_message(lamports, use_lamports_unit, true)), @@ -1051,42 +1096,44 @@ fn process_deploy( let mut messages: Vec<&Message> = Vec::new(); let (blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; let minimum_balance = rpc_client.get_minimum_balance_for_rent_exemption(program_data.len())?; - let mut create_account_tx = system_transaction::create_account( - &config.keypair, - &program_id, - blockhash, + let ix = system_instruction::create_account( + &config.signers[0].pubkey(), + &program_id.pubkey(), minimum_balance.max(1), program_data.len() as u64, &bpf_loader::id(), ); + let message = Message::new(vec![ix]); + let mut create_account_tx = Transaction::new_unsigned(message); + create_account_tx.try_sign(&[config.signers[0], &program_id], blockhash)?; messages.push(&create_account_tx.message); - let signers = [&config.keypair, &program_id]; - let write_transactions: Vec<_> = program_data - .chunks(USERDATA_CHUNK_SIZE) - .zip(0..) - .map(|(chunk, i)| { - let instruction = loader_instruction::write( - &program_id.pubkey(), - &bpf_loader::id(), - (i * USERDATA_CHUNK_SIZE) as u32, - chunk.to_vec(), - ); - let message = Message::new_with_payer(vec![instruction], Some(&signers[0].pubkey())); - Transaction::new(&signers, message, blockhash) - }) - .collect(); + let signers = [config.signers[0], &program_id]; + let mut write_transactions = vec![]; + for (chunk, i) in program_data.chunks(DATA_CHUNK_SIZE).zip(0..) { + let instruction = loader_instruction::write( + &program_id.pubkey(), + &bpf_loader::id(), + (i * DATA_CHUNK_SIZE) as u32, + chunk.to_vec(), + ); + let message = Message::new_with_payer(vec![instruction], Some(&signers[0].pubkey())); + let mut tx = Transaction::new_unsigned(message); + tx.try_sign(&signers, blockhash)?; + write_transactions.push(tx); + } for transaction in write_transactions.iter() { messages.push(&transaction.message); } let instruction = loader_instruction::finalize(&program_id.pubkey(), &bpf_loader::id()); let message = Message::new_with_payer(vec![instruction], Some(&signers[0].pubkey())); - let mut finalize_tx = Transaction::new(&signers, message, blockhash); + let mut finalize_tx = Transaction::new_unsigned(message); + finalize_tx.try_sign(&signers, blockhash)?; messages.push(&finalize_tx.message); check_account_for_multiple_fees( rpc_client, - &config.keypair.pubkey(), + &config.signers[0].pubkey(), &fee_calculator, &messages, )?; @@ -1123,76 +1170,62 @@ fn process_pay( witnesses: &Option>, cancelable: bool, sign_only: bool, - signers: &Option>, blockhash_query: &BlockhashQuery, nonce_account: Option, - nonce_authority: Option<&SigningAuthority>, + nonce_authority: SignerIndex, ) -> ProcessResult { check_unique_pubkeys( - (&config.keypair.pubkey(), "cli keypair".to_string()), + (&config.signers[0].pubkey(), "cli keypair".to_string()), (to, "to".to_string()), )?; let (blockhash, fee_calculator) = blockhash_query.get_blockhash_fee_calculator(rpc_client)?; let cancelable = if cancelable { - Some(config.keypair.pubkey()) + Some(config.signers[0].pubkey()) } else { None }; if timestamp == None && *witnesses == None { - let mut tx = if let Some(nonce_account) = &nonce_account { - let nonce_authority: &Keypair = nonce_authority - .map(|authority| authority.keypair()) - .unwrap_or(&config.keypair); - system_transaction::nonced_transfer( - &config.keypair, - to, - lamports, - nonce_account, - nonce_authority, - blockhash, - ) + let nonce_authority = config.signers[nonce_authority]; + let ix = system_instruction::transfer(&config.signers[0].pubkey(), to, lamports); + let message = if let Some(nonce_account) = &nonce_account { + Message::new_with_nonce(vec![ix], None, nonce_account, &nonce_authority.pubkey()) } else { - system_transaction::transfer(&config.keypair, to, lamports, blockhash) + Message::new(vec![ix]) }; - - if let Some(signers) = signers { - replace_signatures(&mut tx, &signers)?; - } + let mut tx = Transaction::new_unsigned(message); + tx.try_sign(&config.signers, blockhash)?; if sign_only { return_signers(&tx) } else { if let Some(nonce_account) = &nonce_account { - let nonce_authority: Pubkey = nonce_authority - .map(|authority| authority.pubkey()) - .unwrap_or_else(|| config.keypair.pubkey()); let nonce_account = rpc_client.get_account(nonce_account)?; - check_nonce_account(&nonce_account, &nonce_authority, &blockhash)?; + check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &blockhash)?; } check_account_for_fee( rpc_client, - &config.keypair.pubkey(), + &config.signers[0].pubkey(), &fee_calculator, &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::(result) } } else if *witnesses == None { let dt = timestamp.unwrap(); let dt_pubkey = match timestamp_pubkey { Some(pubkey) => pubkey, - None => config.keypair.pubkey(), + None => config.signers[0].pubkey(), }; let contract_state = Keypair::new(); // Initializing contract let ixs = budget_instruction::on_date( - &config.keypair.pubkey(), + &config.signers[0].pubkey(), to, &contract_state.pubkey(), dt, @@ -1200,25 +1233,20 @@ fn process_pay( cancelable, lamports, ); - let mut tx = Transaction::new_signed_instructions( - &[&config.keypair, &contract_state], - ixs, - blockhash, - ); - if let Some(signers) = signers { - replace_signatures(&mut tx, &signers)?; - } + let message = Message::new(ixs); + let mut tx = Transaction::new_unsigned(message); + tx.try_sign(&[config.signers[0], &contract_state], blockhash)?; if sign_only { return_signers(&tx) } else { check_account_for_fee( rpc_client, - &config.keypair.pubkey(), + &config.signers[0].pubkey(), &fee_calculator, &tx.message, )?; let result = rpc_client - .send_and_confirm_transaction(&mut tx, &[&config.keypair, &contract_state]); + .send_and_confirm_transaction(&mut tx, &[config.signers[0], &contract_state]); let signature_str = log_instruction_custom_error::(result)?; Ok(json!({ @@ -1241,29 +1269,24 @@ fn process_pay( // Initializing contract let ixs = budget_instruction::when_signed( - &config.keypair.pubkey(), + &config.signers[0].pubkey(), to, &contract_state.pubkey(), &witness, cancelable, lamports, ); - let mut tx = Transaction::new_signed_instructions( - &[&config.keypair, &contract_state], - ixs, - blockhash, - ); - if let Some(signers) = signers { - replace_signatures(&mut tx, &signers)?; - } + let message = Message::new(ixs); + let mut tx = Transaction::new_unsigned(message); + tx.try_sign(&[config.signers[0], &contract_state], blockhash)?; if sign_only { return_signers(&tx) } else { let result = rpc_client - .send_and_confirm_transaction(&mut tx, &[&config.keypair, &contract_state]); + .send_and_confirm_transaction(&mut tx, &[config.signers[0], &contract_state]); check_account_for_fee( rpc_client, - &config.keypair.pubkey(), + &config.signers[0].pubkey(), &fee_calculator, &tx.message, )?; @@ -1283,18 +1306,20 @@ fn process_pay( fn process_cancel(rpc_client: &RpcClient, config: &CliConfig, pubkey: &Pubkey) -> ProcessResult { let (blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; let ix = budget_instruction::apply_signature( - &config.keypair.pubkey(), + &config.signers[0].pubkey(), pubkey, - &config.keypair.pubkey(), + &config.signers[0].pubkey(), ); - let mut tx = Transaction::new_signed_instructions(&[&config.keypair], vec![ix], blockhash); + let message = Message::new(vec![ix]); + let mut tx = Transaction::new_unsigned(message); + tx.try_sign(&config.signers, blockhash)?; check_account_for_fee( rpc_client, - &config.keypair.pubkey(), + &config.signers[0].pubkey(), &fee_calculator, &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::(result) } @@ -1307,15 +1332,17 @@ fn process_time_elapsed( ) -> ProcessResult { let (blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; - let ix = budget_instruction::apply_timestamp(&config.keypair.pubkey(), pubkey, to, dt); - let mut tx = Transaction::new_signed_instructions(&[&config.keypair], vec![ix], blockhash); + let ix = budget_instruction::apply_timestamp(&config.signers[0].pubkey(), pubkey, to, dt); + let message = Message::new(vec![ix]); + let mut tx = Transaction::new_unsigned(message); + tx.try_sign(&config.signers, blockhash)?; check_account_for_fee( rpc_client, - &config.keypair.pubkey(), + &config.signers[0].pubkey(), &fee_calculator, &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::(result) } @@ -1325,20 +1352,17 @@ fn process_transfer( config: &CliConfig, lamports: u64, to: &Pubkey, - from: Option<&SigningAuthority>, + from: SignerIndex, sign_only: bool, - signers: Option<&Vec<(Pubkey, Signature)>>, blockhash_query: &BlockhashQuery, nonce_account: Option<&Pubkey>, - nonce_authority: Option<&SigningAuthority>, - fee_payer: Option<&SigningAuthority>, + nonce_authority: SignerIndex, + fee_payer: SignerIndex, ) -> ProcessResult { - let (from_pubkey, from) = from - .map(|f| (f.pubkey(), f.keypair())) - .unwrap_or((config.keypair.pubkey(), &config.keypair)); + let from = config.signers[from]; check_unique_pubkeys( - (&from_pubkey, "cli keypair".to_string()), + (&from.pubkey(), "cli keypair".to_string()), (to, "to".to_string()), )?; @@ -1346,38 +1370,28 @@ fn process_transfer( blockhash_query.get_blockhash_fee_calculator(rpc_client)?; let ixs = vec![system_instruction::transfer(&from.pubkey(), to, lamports)]; - let (nonce_authority_pubkey, nonce_authority) = nonce_authority - .map(|authority| (authority.pubkey(), authority.keypair())) - .unwrap_or((config.keypair.pubkey(), &config.keypair)); - let fee_payer = fee_payer.map(|fp| fp.keypair()).unwrap_or(&config.keypair); - let mut tx = if let Some(nonce_account) = &nonce_account { - Transaction::new_signed_with_nonce( + let nonce_authority = config.signers[nonce_authority]; + let fee_payer = config.signers[fee_payer]; + + let message = if let Some(nonce_account) = &nonce_account { + Message::new_with_nonce( ixs, Some(&fee_payer.pubkey()), - &[fee_payer, from, nonce_authority], nonce_account, &nonce_authority.pubkey(), - recent_blockhash, ) } else { - Transaction::new_signed_with_payer( - ixs, - Some(&fee_payer.pubkey()), - &[fee_payer, from], - recent_blockhash, - ) + Message::new_with_payer(ixs, Some(&fee_payer.pubkey())) }; - - if let Some(signers) = signers { - replace_signatures(&mut tx, &signers)?; - } + let mut tx = Transaction::new_unsigned(message); + tx.try_sign(&config.signers, recent_blockhash)?; if sign_only { return_signers(&tx) } else { if let Some(nonce_account) = &nonce_account { let nonce_account = rpc_client.get_account(nonce_account)?; - check_nonce_account(&nonce_account, &nonce_authority_pubkey, &recent_blockhash)?; + check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?; } check_account_for_fee( rpc_client, @@ -1385,7 +1399,7 @@ fn process_transfer( &fee_calculator, &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::(result) } } @@ -1398,26 +1412,26 @@ fn process_witness( ) -> ProcessResult { let (blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; - let ix = budget_instruction::apply_signature(&config.keypair.pubkey(), pubkey, to); - let mut tx = Transaction::new_signed_instructions(&[&config.keypair], vec![ix], blockhash); + let ix = budget_instruction::apply_signature(&config.signers[0].pubkey(), pubkey, to); + let message = Message::new(vec![ix]); + let mut tx = Transaction::new_unsigned(message); + tx.try_sign(&config.signers, blockhash)?; check_account_for_fee( rpc_client, - &config.keypair.pubkey(), + &config.signers[0].pubkey(), &fee_calculator, &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::(result) } pub fn process_command(config: &CliConfig) -> ProcessResult { if config.verbose { println_name_value("RPC URL:", &config.json_rpc_url); - if let Some(keypair_path) = &config.keypair_path { - println_name_value("Keypair Path:", keypair_path); - if keypair_path.starts_with("usb://") { - println_name_value("Pubkey:", &format!("{:?}", config.pubkey()?)); - } + println_name_value("Default Signer Path:", &config.keypair_path); + if config.keypair_path.starts_with("usb://") { + println_name_value("Pubkey:", &format!("{:?}", config.pubkey()?)); } } @@ -1492,13 +1506,13 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { // Assign authority to nonce account CliCommand::AuthorizeNonceAccount { nonce_account, - ref nonce_authority, + nonce_authority, new_authority, } => process_authorize_nonce_account( &rpc_client, config, nonce_account, - nonce_authority.as_ref(), + *nonce_authority, new_authority, ), // Create nonce account @@ -1510,7 +1524,7 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { } => process_create_nonce_account( &rpc_client, config, - nonce_account, + *nonce_account, seed.clone(), *nonce_authority, *lamports, @@ -1522,8 +1536,8 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { // Get a new nonce CliCommand::NewNonce { nonce_account, - ref nonce_authority, - } => process_new_nonce(&rpc_client, config, nonce_account, nonce_authority.as_ref()), + nonce_authority, + } => process_new_nonce(&rpc_client, config, nonce_account, *nonce_authority), // Show the contents of a nonce account CliCommand::ShowNonceAccount { nonce_account_pubkey, @@ -1532,14 +1546,14 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { // Withdraw lamports from a nonce account CliCommand::WithdrawFromNonceAccount { nonce_account, - ref nonce_authority, + nonce_authority, destination_account_pubkey, lamports, } => process_withdraw_from_nonce_account( &rpc_client, config, &nonce_account, - nonce_authority.as_ref(), + *nonce_authority, &destination_account_pubkey, *lamports, ), @@ -1555,108 +1569,100 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { // Create stake account CliCommand::CreateStakeAccount { - ref stake_account, - seed, - staker, - withdrawer, - lockup, - lamports, - sign_only, - ref signers, - blockhash_query, - ref nonce_account, - ref nonce_authority, - ref fee_payer, - ref from, - } => process_create_stake_account( - &rpc_client, - config, stake_account, seed, staker, withdrawer, lockup, + lamports, + sign_only, + blockhash_query, + ref nonce_account, + nonce_authority, + fee_payer, + from, + } => process_create_stake_account( + &rpc_client, + config, + *stake_account, + seed, + staker, + withdrawer, + lockup, *lamports, *sign_only, - signers.as_ref(), blockhash_query, nonce_account.as_ref(), - nonce_authority.as_ref(), - fee_payer.as_ref(), - from.as_ref(), + *nonce_authority, + *fee_payer, + *from, ), CliCommand::DeactivateStake { stake_account_pubkey, - ref stake_authority, + stake_authority, sign_only, - ref signers, blockhash_query, nonce_account, - ref nonce_authority, - ref fee_payer, + nonce_authority, + fee_payer, } => process_deactivate_stake_account( &rpc_client, config, &stake_account_pubkey, - stake_authority.as_ref(), + *stake_authority, *sign_only, - signers, blockhash_query, *nonce_account, - nonce_authority.as_ref(), - fee_payer.as_ref(), + *nonce_authority, + *fee_payer, ), CliCommand::DelegateStake { stake_account_pubkey, vote_account_pubkey, - ref stake_authority, + stake_authority, force, sign_only, - ref signers, blockhash_query, nonce_account, - ref nonce_authority, - ref fee_payer, + nonce_authority, + fee_payer, } => process_delegate_stake( &rpc_client, config, &stake_account_pubkey, &vote_account_pubkey, - stake_authority.as_ref(), + *stake_authority, *force, *sign_only, - signers, blockhash_query, *nonce_account, - nonce_authority.as_ref(), - fee_payer.as_ref(), + *nonce_authority, + *fee_payer, ), CliCommand::SplitStake { stake_account_pubkey, - ref stake_authority, + stake_authority, sign_only, - ref signers, blockhash_query, nonce_account, - ref nonce_authority, + nonce_authority, split_stake_account, seed, lamports, - ref fee_payer, + fee_payer, } => process_split_stake( &rpc_client, config, &stake_account_pubkey, - stake_authority.as_ref(), + *stake_authority, *sign_only, - signers, blockhash_query, *nonce_account, - nonce_authority.as_ref(), - split_stake_account, + *nonce_authority, + *split_stake_account, seed, *lamports, - fee_payer.as_ref(), + *fee_payer, ), CliCommand::ShowStakeAccount { pubkey: stake_account_pubkey, @@ -1674,74 +1680,68 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { stake_account_pubkey, new_authorized_pubkey, stake_authorize, - ref authority, + authority, sign_only, - ref signers, blockhash_query, nonce_account, - ref nonce_authority, - ref fee_payer, + nonce_authority, + fee_payer, } => process_stake_authorize( &rpc_client, config, &stake_account_pubkey, &new_authorized_pubkey, *stake_authorize, - authority.as_ref(), + *authority, *sign_only, - signers, blockhash_query, *nonce_account, - nonce_authority.as_ref(), - fee_payer.as_ref(), + *nonce_authority, + *fee_payer, ), CliCommand::StakeSetLockup { stake_account_pubkey, mut lockup, - ref custodian, + custodian, sign_only, - ref signers, blockhash_query, nonce_account, - ref nonce_authority, - ref fee_payer, + nonce_authority, + fee_payer, } => process_stake_set_lockup( &rpc_client, config, &stake_account_pubkey, &mut lockup, - custodian.as_ref(), + *custodian, *sign_only, - signers, blockhash_query, *nonce_account, - nonce_authority.as_ref(), - fee_payer.as_ref(), + *nonce_authority, + *fee_payer, ), CliCommand::WithdrawStake { stake_account_pubkey, destination_account_pubkey, lamports, - ref withdraw_authority, + withdraw_authority, sign_only, - ref signers, blockhash_query, ref nonce_account, - ref nonce_authority, - ref fee_payer, + nonce_authority, + fee_payer, } => process_withdraw_stake( &rpc_client, config, &stake_account_pubkey, &destination_account_pubkey, *lamports, - withdraw_authority.as_ref(), + *withdraw_authority, *sign_only, - signers.as_ref(), blockhash_query, nonce_account.as_ref(), - nonce_authority.as_ref(), - fee_payer.as_ref(), + *nonce_authority, + *fee_payer, ), // Storage Commands @@ -1754,8 +1754,8 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { } => process_create_storage_account( &rpc_client, config, + *storage_account, &account_owner, - storage_account, *account_type, ), CliCommand::ClaimStorageReward { @@ -1794,7 +1794,6 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { // Create vote account CliCommand::CreateVoteAccount { - vote_account, seed, node_pubkey, authorized_voter, @@ -1803,7 +1802,6 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { } => process_create_vote_account( &rpc_client, config, - vote_account, seed, &node_pubkey, authorized_voter, @@ -1833,13 +1831,11 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { CliCommand::VoteUpdateValidator { vote_account_pubkey, new_identity_pubkey, - authorized_voter, } => process_vote_update_validator( &rpc_client, config, &vote_account_pubkey, &new_identity_pubkey, - authorized_voter, ), // Wallet Commands @@ -1885,10 +1881,9 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { ref witnesses, cancelable, sign_only, - ref signers, blockhash_query, nonce_account, - ref nonce_authority, + nonce_authority, }) => process_pay( &rpc_client, config, @@ -1899,10 +1894,9 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { witnesses, *cancelable, *sign_only, - signers, blockhash_query, *nonce_account, - nonce_authority.as_ref(), + *nonce_authority, ), CliCommand::ShowAccount { pubkey, @@ -1922,25 +1916,23 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { CliCommand::Transfer { lamports, to, - ref from, + from, sign_only, - ref signers, ref blockhash_query, ref nonce_account, - ref nonce_authority, - ref fee_payer, + nonce_authority, + fee_payer, } => process_transfer( &rpc_client, config, *lamports, to, - from.as_ref(), + *from, *sign_only, - signers.as_ref(), blockhash_query, nonce_account.as_ref(), - nonce_authority.as_ref(), - fee_payer.as_ref(), + *nonce_authority, + *fee_payer, ), // Apply witness signature to contract CliCommand::Witness(to, pubkey) => process_witness(&rpc_client, config, &to, &pubkey), @@ -2061,7 +2053,16 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' .about(about) .version(version) .setting(AppSettings::SubcommandRequiredElseHelp) - .subcommand(SubCommand::with_name("address").about("Get your public key")) + .subcommand( + SubCommand::with_name("address") + .about("Get your public key") + .arg( + Arg::with_name("confirm_key") + .long("confirm-key") + .takes_value(false) + .help("Confirm key on device; only relevant if using remote wallet"), + ), + ) .cluster_query_subcommands() .nonce_subcommands() .stake_subcommands() @@ -2110,7 +2111,7 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' .index(1) .value_name("PUBKEY") .takes_value(true) - .validator(is_pubkey_or_keypair) + .validator(is_valid_signer) .help("The public key of the balance to check"), ) .arg( @@ -2319,8 +2320,8 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' Arg::with_name("from") .long("from") .takes_value(true) - .value_name("KEYPAIR or PUBKEY") - .validator(is_pubkey_or_keypair_or_ask_keyword) + .value_name("KEYPAIR or PUBKEY or REMOTE WALLET PATH") + .validator(is_valid_signer) .help("Source account of funds (if different from client local account)"), ) .offline_args() @@ -2373,7 +2374,7 @@ mod tests { account::Account, nonce_state::{Meta as NonceMeta, NonceState}, pubkey::Pubkey, - signature::{keypair_from_seed, read_keypair_file, write_keypair_file}, + signature::{keypair_from_seed, read_keypair_file, write_keypair_file, Presigner}, system_program, transaction::TransactionError, }; @@ -2394,10 +2395,68 @@ mod tests { } #[test] - fn test_signing_authority_dummy_keypairs() { - let signing_authority: SigningAuthority = Pubkey::new(&[1u8; 32]).into(); - assert_eq!(signing_authority, Pubkey::new(&[1u8; 32]).into()); - assert_ne!(signing_authority, Pubkey::new(&[2u8; 32]).into()); + fn test_generate_unique_signers() { + let matches = ArgMatches::default(); + + let default_keypair = Keypair::new(); + let default_keypair_file = make_tmp_path("keypair_file"); + write_keypair_file(&default_keypair, &default_keypair_file).unwrap(); + + let signer_info = + generate_unique_signers(vec![], &matches, &default_keypair_file, None).unwrap(); + assert_eq!(signer_info.signers.len(), 0); + + let signer_info = + generate_unique_signers(vec![None, None], &matches, &default_keypair_file, None) + .unwrap(); + assert_eq!(signer_info.signers.len(), 1); + assert_eq!(signer_info.index_of(None), Some(0)); + assert_eq!(signer_info.index_of(Some(Pubkey::new_rand())), None); + + let keypair0 = keypair_from_seed(&[1u8; 32]).unwrap(); + let keypair0_pubkey = keypair0.pubkey(); + let keypair0_clone = keypair_from_seed(&[1u8; 32]).unwrap(); + let keypair0_clone_pubkey = keypair0.pubkey(); + let signers = vec![None, Some(keypair0.into()), Some(keypair0_clone.into())]; + let signer_info = + generate_unique_signers(signers, &matches, &default_keypair_file, None).unwrap(); + assert_eq!(signer_info.signers.len(), 2); + assert_eq!(signer_info.index_of(None), Some(0)); + assert_eq!(signer_info.index_of(Some(keypair0_pubkey)), Some(1)); + assert_eq!(signer_info.index_of(Some(keypair0_clone_pubkey)), Some(1)); + + let keypair0 = keypair_from_seed(&[1u8; 32]).unwrap(); + let keypair0_pubkey = keypair0.pubkey(); + let keypair0_clone = keypair_from_seed(&[1u8; 32]).unwrap(); + let signers = vec![Some(keypair0.into()), Some(keypair0_clone.into())]; + let signer_info = + generate_unique_signers(signers, &matches, &default_keypair_file, None).unwrap(); + assert_eq!(signer_info.signers.len(), 1); + assert_eq!(signer_info.index_of(Some(keypair0_pubkey)), Some(0)); + + // Signers with the same pubkey are not distinct + let keypair0 = keypair_from_seed(&[2u8; 32]).unwrap(); + let keypair0_pubkey = keypair0.pubkey(); + let keypair1 = keypair_from_seed(&[3u8; 32]).unwrap(); + let keypair1_pubkey = keypair1.pubkey(); + let message = vec![0, 1, 2, 3]; + let presigner0 = Presigner::new(&keypair0.pubkey(), &keypair0.sign_message(&message)); + let presigner0_pubkey = presigner0.pubkey(); + let presigner1 = Presigner::new(&keypair1.pubkey(), &keypair1.sign_message(&message)); + let presigner1_pubkey = presigner1.pubkey(); + let signers = vec![ + Some(keypair0.into()), + Some(presigner0.into()), + Some(presigner1.into()), + Some(keypair1.into()), + ]; + let signer_info = + generate_unique_signers(signers, &matches, &default_keypair_file, None).unwrap(); + assert_eq!(signer_info.signers.len(), 2); + assert_eq!(signer_info.index_of(Some(keypair0_pubkey)), Some(0)); + assert_eq!(signer_info.index_of(Some(keypair1_pubkey)), Some(1)); + assert_eq!(signer_info.index_of(Some(presigner0_pubkey)), Some(0)); + assert_eq!(signer_info.index_of(Some(presigner1_pubkey)), Some(1)); } #[test] @@ -2418,7 +2477,7 @@ mod tests { .clone() .get_matches_from(vec!["test", "airdrop", "50", &pubkey_string]); assert_eq!( - parse_command(&test_airdrop).unwrap(), + parse_command(&test_airdrop, "", None).unwrap(), CliCommandInfo { command: CliCommand::Airdrop { faucet_host: None, @@ -2426,13 +2485,14 @@ mod tests { pubkey: Some(pubkey), lamports: 50_000_000_000, }, - require_keypair: true, + signers: vec![], } ); // Test Balance Subcommand, incl pubkey and keypair-file inputs + let default_keypair = Keypair::new(); let keypair_file = make_tmp_path("keypair_file"); - write_keypair_file(&Keypair::new(), &keypair_file).unwrap(); + write_keypair_file(&default_keypair, &keypair_file).unwrap(); let keypair = read_keypair_file(&keypair_file).unwrap(); let test_balance = test_commands.clone().get_matches_from(vec![ "test", @@ -2440,13 +2500,13 @@ mod tests { &keypair.pubkey().to_string(), ]); assert_eq!( - parse_command(&test_balance).unwrap(), + parse_command(&test_balance, "", None).unwrap(), CliCommandInfo { command: CliCommand::Balance { pubkey: Some(keypair.pubkey()), use_lamports_unit: false }, - require_keypair: false + signers: vec![], } ); let test_balance = test_commands.clone().get_matches_from(vec![ @@ -2456,13 +2516,13 @@ mod tests { "--lamports", ]); assert_eq!( - parse_command(&test_balance).unwrap(), + parse_command(&test_balance, "", None).unwrap(), CliCommandInfo { command: CliCommand::Balance { pubkey: Some(keypair.pubkey()), use_lamports_unit: true }, - require_keypair: false + signers: vec![], } ); let test_balance = @@ -2470,13 +2530,13 @@ mod tests { .clone() .get_matches_from(vec!["test", "balance", "--lamports"]); assert_eq!( - parse_command(&test_balance).unwrap(), + parse_command(&test_balance, &keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::Balance { pubkey: None, use_lamports_unit: true }, - require_keypair: true + signers: vec![read_keypair_file(&keypair_file).unwrap().into()], } ); @@ -2486,10 +2546,10 @@ mod tests { .clone() .get_matches_from(vec!["test", "cancel", &pubkey_string]); assert_eq!( - parse_command(&test_cancel).unwrap(), + parse_command(&test_cancel, &keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::Cancel(pubkey), - require_keypair: true + signers: vec![read_keypair_file(&keypair_file).unwrap().into()], } ); @@ -2501,16 +2561,16 @@ mod tests { .clone() .get_matches_from(vec!["test", "confirm", &signature_string]); assert_eq!( - parse_command(&test_confirm).unwrap(), + parse_command(&test_confirm, "", None).unwrap(), CliCommandInfo { command: CliCommand::Confirm(signature), - require_keypair: false + signers: vec![], } ); let test_bad_signature = test_commands .clone() .get_matches_from(vec!["test", "confirm", "deadbeef"]); - assert!(parse_command(&test_bad_signature).is_err()); + assert!(parse_command(&test_bad_signature, "", None).is_err()); // Test CreateAddressWithSeed let from_pubkey = Some(Pubkey::new_rand()); @@ -2529,14 +2589,14 @@ mod tests { &from_str, ]); assert_eq!( - parse_command(&test_create_address_with_seed).unwrap(), + parse_command(&test_create_address_with_seed, "", None).unwrap(), CliCommandInfo { command: CliCommand::CreateAddressWithSeed { from_pubkey, seed: "seed".to_string(), program_id: *program_id }, - require_keypair: false + signers: vec![], } ); } @@ -2547,14 +2607,14 @@ mod tests { "STAKE", ]); assert_eq!( - parse_command(&test_create_address_with_seed).unwrap(), + parse_command(&test_create_address_with_seed, "", None).unwrap(), CliCommandInfo { command: CliCommand::CreateAddressWithSeed { from_pubkey: None, seed: "seed".to_string(), program_id: solana_stake_program::id(), }, - require_keypair: true + signers: vec![], } ); @@ -2564,10 +2624,10 @@ mod tests { .clone() .get_matches_from(vec!["test", "deploy", "/Users/test/program.o"]); assert_eq!( - parse_command(&test_deploy).unwrap(), + parse_command(&test_deploy, &keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::Deploy("/Users/test/program.o".to_string()), - require_keypair: true + signers: vec![read_keypair_file(&keypair_file).unwrap().into()], } ); @@ -2577,14 +2637,14 @@ mod tests { .clone() .get_matches_from(vec!["test", "pay", &pubkey_string, "50"]); assert_eq!( - parse_command(&test_pay).unwrap(), + parse_command(&test_pay, &keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::Pay(PayCommand { lamports: 50_000_000_000, to: pubkey, ..PayCommand::default() }), - require_keypair: true + signers: vec![read_keypair_file(&keypair_file).unwrap().into()], } ); @@ -2600,7 +2660,7 @@ mod tests { &witness1_string, ]); assert_eq!( - parse_command(&test_pay_multiple_witnesses).unwrap(), + parse_command(&test_pay_multiple_witnesses, &keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::Pay(PayCommand { lamports: 50_000_000_000, @@ -2608,7 +2668,7 @@ mod tests { witnesses: Some(vec![witness0, witness1]), ..PayCommand::default() }), - require_keypair: true + signers: vec![read_keypair_file(&keypair_file).unwrap().into()], } ); let test_pay_single_witness = test_commands.clone().get_matches_from(vec![ @@ -2620,7 +2680,7 @@ mod tests { &witness0_string, ]); assert_eq!( - parse_command(&test_pay_single_witness).unwrap(), + parse_command(&test_pay_single_witness, &keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::Pay(PayCommand { lamports: 50_000_000_000, @@ -2628,7 +2688,7 @@ mod tests { witnesses: Some(vec![witness0]), ..PayCommand::default() }), - require_keypair: true + signers: vec![read_keypair_file(&keypair_file).unwrap().into()], } ); @@ -2644,7 +2704,7 @@ mod tests { &witness0_string, ]); assert_eq!( - parse_command(&test_pay_timestamp).unwrap(), + parse_command(&test_pay_timestamp, &keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::Pay(PayCommand { lamports: 50_000_000_000, @@ -2653,7 +2713,7 @@ mod tests { timestamp_pubkey: Some(witness0), ..PayCommand::default() }), - require_keypair: true + signers: vec![read_keypair_file(&keypair_file).unwrap().into()], } ); @@ -2670,7 +2730,7 @@ mod tests { "--sign-only", ]); assert_eq!( - parse_command(&test_pay).unwrap(), + parse_command(&test_pay, &keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::Pay(PayCommand { lamports: 50_000_000_000, @@ -2679,65 +2739,7 @@ mod tests { sign_only: true, ..PayCommand::default() }), - require_keypair: true, - } - ); - - // Test Pay Subcommand w/ signer - let key1 = Pubkey::new_rand(); - let sig1 = Keypair::new().sign_message(&[0u8]); - let signer1 = format!("{}={}", key1, sig1); - let test_pay = test_commands.clone().get_matches_from(vec![ - "test", - "pay", - &pubkey_string, - "50", - "--blockhash", - &blockhash_string, - "--signer", - &signer1, - ]); - assert_eq!( - parse_command(&test_pay).unwrap(), - CliCommandInfo { - command: CliCommand::Pay(PayCommand { - lamports: 50_000_000_000, - to: pubkey, - blockhash_query: BlockhashQuery::FeeCalculator(blockhash), - signers: Some(vec![(key1, sig1)]), - ..PayCommand::default() - }), - require_keypair: true - } - ); - - // Test Pay Subcommand w/ signers - let key2 = Pubkey::new_rand(); - let sig2 = Keypair::new().sign_message(&[1u8]); - let signer2 = format!("{}={}", key2, sig2); - let test_pay = test_commands.clone().get_matches_from(vec![ - "test", - "pay", - &pubkey_string, - "50", - "--blockhash", - &blockhash_string, - "--signer", - &signer1, - "--signer", - &signer2, - ]); - assert_eq!( - parse_command(&test_pay).unwrap(), - CliCommandInfo { - command: CliCommand::Pay(PayCommand { - lamports: 50_000_000_000, - to: pubkey, - blockhash_query: BlockhashQuery::FeeCalculator(blockhash), - signers: Some(vec![(key1, sig1), (key2, sig2)]), - ..PayCommand::default() - }), - require_keypair: true + signers: vec![read_keypair_file(&keypair_file).unwrap().into()], } ); @@ -2751,7 +2753,7 @@ mod tests { &blockhash_string, ]); assert_eq!( - parse_command(&test_pay).unwrap(), + parse_command(&test_pay, &keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::Pay(PayCommand { lamports: 50_000_000_000, @@ -2759,7 +2761,7 @@ mod tests { blockhash_query: BlockhashQuery::FeeCalculator(blockhash), ..PayCommand::default() }), - require_keypair: true + signers: vec![read_keypair_file(&keypair_file).unwrap().into()], } ); @@ -2777,7 +2779,7 @@ mod tests { &pubkey_string, ]); assert_eq!( - parse_command(&test_pay).unwrap(), + parse_command(&test_pay, &keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::Pay(PayCommand { lamports: 50_000_000_000, @@ -2786,7 +2788,7 @@ mod tests { nonce_account: Some(pubkey), ..PayCommand::default() }), - require_keypair: true + signers: vec![read_keypair_file(&keypair_file).unwrap().into()], } ); @@ -2807,17 +2809,17 @@ mod tests { &keypair_file, ]); assert_eq!( - parse_command(&test_pay).unwrap(), + parse_command(&test_pay, &keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::Pay(PayCommand { lamports: 50_000_000_000, to: pubkey, blockhash_query: BlockhashQuery::FeeCalculator(blockhash), nonce_account: Some(pubkey), - nonce_authority: Some(keypair.into()), + nonce_authority: 0, ..PayCommand::default() }), - require_keypair: true + signers: vec![keypair.into()], } ); @@ -2842,18 +2844,17 @@ mod tests { &signer_arg, ]); assert_eq!( - parse_command(&test_pay).unwrap(), + parse_command(&test_pay, &keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::Pay(PayCommand { lamports: 50_000_000_000, to: pubkey, blockhash_query: BlockhashQuery::FeeCalculator(blockhash), nonce_account: Some(pubkey), - nonce_authority: Some(authority_pubkey.into()), - signers: Some(vec![(authority_pubkey, sig)]), + nonce_authority: 0, ..PayCommand::default() }), - require_keypair: true + signers: vec![keypair.into()], } ); @@ -2878,7 +2879,7 @@ mod tests { "--signer", &signer_arg, ]); - assert!(parse_command(&test_pay).is_err()); + assert!(parse_command(&test_pay, &keypair_file, None).is_err()); // Test Send-Signature Subcommand let test_send_signature = test_commands.clone().get_matches_from(vec![ @@ -2888,10 +2889,10 @@ mod tests { &pubkey_string, ]); assert_eq!( - parse_command(&test_send_signature).unwrap(), + parse_command(&test_send_signature, &keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::Witness(pubkey, pubkey), - require_keypair: true + signers: vec![read_keypair_file(&keypair_file).unwrap().into()], } ); let test_pay_multiple_witnesses = test_commands.clone().get_matches_from(vec![ @@ -2909,7 +2910,7 @@ mod tests { &witness1_string, ]); assert_eq!( - parse_command(&test_pay_multiple_witnesses).unwrap(), + parse_command(&test_pay_multiple_witnesses, &keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::Pay(PayCommand { lamports: 50_000_000_000, @@ -2919,7 +2920,7 @@ mod tests { witnesses: Some(vec![witness0, witness1]), ..PayCommand::default() }), - require_keypair: true + signers: vec![read_keypair_file(&keypair_file).unwrap().into()], } ); @@ -2933,10 +2934,10 @@ mod tests { "2018-09-19T17:30:59", ]); assert_eq!( - parse_command(&test_send_timestamp).unwrap(), + parse_command(&test_send_timestamp, &keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::TimeElapsed(pubkey, pubkey, dt), - require_keypair: true + signers: vec![read_keypair_file(&keypair_file).unwrap().into()], } ); let test_bad_timestamp = test_commands.clone().get_matches_from(vec![ @@ -2947,7 +2948,7 @@ mod tests { "--date", "20180919T17:30:59", ]); - assert!(parse_command(&test_bad_timestamp).is_err()); + assert!(parse_command(&test_bad_timestamp, &keypair_file, None).is_err()); } #[test] @@ -2958,7 +2959,7 @@ mod tests { let keypair = Keypair::new(); let pubkey = keypair.pubkey().to_string(); - config.keypair = keypair; + config.signers = vec![&keypair]; config.command = CliCommand::Address; assert_eq!(process_command(&config).unwrap(), pubkey); @@ -2986,17 +2987,18 @@ mod tests { let bob_pubkey = bob_keypair.pubkey(); let node_pubkey = Pubkey::new_rand(); config.command = CliCommand::CreateVoteAccount { - vote_account: bob_keypair.into(), seed: None, node_pubkey, authorized_voter: Some(bob_pubkey), authorized_withdrawer: Some(bob_pubkey), commission: 0, }; + config.signers = vec![&keypair, &bob_keypair]; let signature = process_command(&config); assert_eq!(signature.unwrap(), SIGNATURE.to_string()); let new_authorized_pubkey = Pubkey::new_rand(); + config.signers = vec![&bob_keypair]; config.command = CliCommand::VoteAuthorize { vote_account_pubkey: bob_pubkey, new_authorized_pubkey, @@ -3006,10 +3008,10 @@ mod tests { assert_eq!(signature.unwrap(), SIGNATURE.to_string()); let new_identity_pubkey = Pubkey::new_rand(); + config.signers = vec![&keypair, &bob_keypair]; config.command = CliCommand::VoteUpdateValidator { vote_account_pubkey: bob_pubkey, new_identity_pubkey, - authorized_voter: Keypair::new().into(), }; let signature = process_command(&config); assert_eq!(signature.unwrap(), SIGNATURE.to_string()); @@ -3018,7 +3020,7 @@ mod tests { let bob_pubkey = bob_keypair.pubkey(); let custodian = Pubkey::new_rand(); config.command = CliCommand::CreateStakeAccount { - stake_account: bob_keypair.into(), + stake_account: 1, seed: None, staker: None, withdrawer: None, @@ -3029,13 +3031,13 @@ mod tests { }, lamports: 1234, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::All, nonce_account: None, - nonce_authority: None, - fee_payer: None, - from: None, + nonce_authority: 0, + fee_payer: 0, + from: 0, }; + config.signers = vec![&keypair, &bob_keypair]; let signature = process_command(&config); assert_eq!(signature.unwrap(), SIGNATURE.to_string()); @@ -3045,27 +3047,26 @@ mod tests { stake_account_pubkey: stake_pubkey, destination_account_pubkey: to_pubkey, lamports: 100, - withdraw_authority: None, + withdraw_authority: 0, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::All, nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }; + config.signers = vec![&keypair]; let signature = process_command(&config); assert_eq!(signature.unwrap(), SIGNATURE.to_string()); let stake_pubkey = Pubkey::new_rand(); config.command = CliCommand::DeactivateStake { stake_account_pubkey: stake_pubkey, - stake_authority: None, + stake_authority: 0, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::default(), nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }; let signature = process_command(&config); assert_eq!(signature.unwrap(), SIGNATURE.to_string()); @@ -3074,17 +3075,17 @@ mod tests { let split_stake_account = Keypair::new(); config.command = CliCommand::SplitStake { stake_account_pubkey: stake_pubkey, - stake_authority: None, + stake_authority: 0, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::default(), nonce_account: None, - nonce_authority: None, - split_stake_account: split_stake_account.into(), + nonce_authority: 0, + split_stake_account: 1, seed: None, lamports: 1234, - fee_payer: None, + fee_payer: 0, }; + config.signers = vec![&keypair, &split_stake_account]; let signature = process_command(&config); assert_eq!(signature.unwrap(), SIGNATURE.to_string()); @@ -3098,6 +3099,7 @@ mod tests { }; assert_eq!(process_command(&config).unwrap(), "1234"); + config.signers = vec![&keypair]; config.command = CliCommand::Pay(PayCommand { lamports: 10, to: bob_pubkey, @@ -3112,7 +3114,7 @@ mod tests { lamports: 10, to: bob_pubkey, timestamp: Some(dt), - timestamp_pubkey: Some(config.keypair.pubkey()), + timestamp_pubkey: Some(config.signers[0].pubkey()), ..PayCommand::default() }); let result = process_command(&config); @@ -3154,7 +3156,10 @@ mod tests { value: json!(RpcAccount::encode( Account::new_data( 1, - &NonceState::Initialized(NonceMeta::new(&config.keypair.pubkey()), blockhash), + &NonceState::Initialized( + NonceMeta::new(&config.signers[0].pubkey()), + blockhash + ), &system_program::ID, ) .unwrap() @@ -3196,14 +3201,16 @@ mod tests { to: bob_pubkey, blockhash_query: BlockhashQuery::FeeCalculator(blockhash), nonce_account: Some(bob_pubkey), - nonce_authority: Some(bob_keypair.into()), + nonce_authority: 1, ..PayCommand::default() }); + config.signers = vec![&keypair, &bob_keypair]; let signature = process_command(&config); assert_eq!(signature.unwrap(), SIGNATURE.to_string()); let process_id = Pubkey::new_rand(); config.command = CliCommand::TimeElapsed(bob_pubkey, process_id, dt); + config.signers = vec![&keypair]; let signature = process_command(&config); assert_eq!(signature.unwrap(), SIGNATURE.to_string()); @@ -3268,13 +3275,13 @@ mod tests { let bob_keypair = Keypair::new(); config.command = CliCommand::CreateVoteAccount { - vote_account: bob_keypair.into(), seed: None, node_pubkey, authorized_voter: Some(bob_pubkey), authorized_withdrawer: Some(bob_pubkey), commission: 0, }; + config.signers = vec![&keypair, &bob_keypair]; assert!(process_command(&config).is_err()); config.command = CliCommand::VoteAuthorize { @@ -3287,7 +3294,6 @@ mod tests { config.command = CliCommand::VoteUpdateValidator { vote_account_pubkey: bob_pubkey, new_identity_pubkey: bob_pubkey, - authorized_voter: Keypair::new().into(), }; assert!(process_command(&config).is_err()); @@ -3312,7 +3318,7 @@ mod tests { lamports: 10, to: bob_pubkey, timestamp: Some(dt), - timestamp_pubkey: Some(config.keypair.pubkey()), + timestamp_pubkey: Some(config.signers[0].pubkey()), ..PayCommand::default() }); assert!(process_command(&config).is_err()); @@ -3342,6 +3348,8 @@ mod tests { // Success case let mut config = CliConfig::default(); config.rpc_client = Some(RpcClient::new_mock("deploy_succeeds".to_string())); + let default_keypair = Keypair::new(); + config.signers = vec![&default_keypair]; config.command = CliCommand::Deploy(pathbuf.to_str().unwrap().to_string()); let result = process_command(&config); @@ -3365,6 +3373,10 @@ mod tests { fn test_parse_transfer_subcommand() { let test_commands = app("test", "desc", "version"); + let default_keypair = Keypair::new(); + let default_keypair_file = make_tmp_path("keypair_file"); + write_keypair_file(&default_keypair, &default_keypair_file).unwrap(); + //Test Transfer Subcommand, SOL let from_keypair = keypair_from_seed(&[0u8; 32]).unwrap(); let from_pubkey = from_keypair.pubkey(); @@ -3376,20 +3388,19 @@ mod tests { .clone() .get_matches_from(vec!["test", "transfer", &to_string, "42"]); assert_eq!( - parse_command(&test_transfer).unwrap(), + parse_command(&test_transfer, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::Transfer { lamports: 42_000_000_000, to: to_pubkey, - from: None, + from: 0, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::All, nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }, - require_keypair: true, + signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()], } ); @@ -3406,20 +3417,19 @@ mod tests { "--sign-only", ]); assert_eq!( - parse_command(&test_transfer).unwrap(), + parse_command(&test_transfer, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::Transfer { lamports: 42_000_000_000, to: to_pubkey, - from: None, + from: 0, sign_only: true, - signers: None, blockhash_query: BlockhashQuery::None(blockhash, FeeCalculator::default()), nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }, - require_keypair: true, + signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()], } ); @@ -3441,20 +3451,19 @@ mod tests { &blockhash_string, ]); assert_eq!( - parse_command(&test_transfer).unwrap(), + parse_command(&test_transfer, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::Transfer { lamports: 42_000_000_000, to: to_pubkey, - from: Some(from_pubkey.into()), + from: 0, sign_only: false, - signers: Some(vec![(from_pubkey, from_sig)]), blockhash_query: BlockhashQuery::FeeCalculator(blockhash), nonce_account: None, - nonce_authority: None, - fee_payer: Some(from_pubkey.into()), + nonce_authority: 0, + fee_payer: 0, }, - require_keypair: true, + signers: vec![Presigner::new(&from_pubkey, &from_sig).into()], } ); @@ -3477,20 +3486,22 @@ mod tests { &nonce_authority_file, ]); assert_eq!( - parse_command(&test_transfer).unwrap(), + parse_command(&test_transfer, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::Transfer { lamports: 42_000_000_000, to: to_pubkey, - from: None, + from: 0, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::FeeCalculator(blockhash), nonce_account: Some(nonce_address.into()), - nonce_authority: Some(read_keypair_file(&nonce_authority_file).unwrap().into()), - fee_payer: None, + nonce_authority: 1, + fee_payer: 0, }, - require_keypair: true, + signers: vec![ + read_keypair_file(&default_keypair_file).unwrap().into(), + read_keypair_file(&nonce_authority_file).unwrap().into() + ], } ); } diff --git a/cli/src/cluster_query.rs b/cli/src/cluster_query.rs index bbccf784c8..b5cce3c5dd 100644 --- a/cli/src/cluster_query.rs +++ b/cli/src/cluster_query.rs @@ -8,21 +8,25 @@ use crate::{ use clap::{value_t, value_t_or_exit, App, Arg, ArgMatches, SubCommand}; use console::{style, Emoji}; 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_remote_wallet::remote_wallet::RemoteWalletManager; use solana_sdk::{ account_utils::StateMut, clock::{self, Slot}, commitment_config::CommitmentConfig, epoch_schedule::Epoch, hash::Hash, + message::Message, pubkey::Pubkey, signature::{Keypair, Signer}, - system_transaction, + system_instruction, + transaction::Transaction, }; use std::{ collections::{HashMap, VecDeque}, net::SocketAddr, + sync::Arc, thread::sleep, time::{Duration, Instant}, }; @@ -216,11 +220,15 @@ pub fn parse_catchup(matches: &ArgMatches<'_>) -> Result) -> Result { +pub fn parse_cluster_ping( + matches: &ArgMatches<'_>, + default_signer_path: &str, + wallet_manager: Option<&Arc>, +) -> Result { let lamports = value_t_or_exit!(matches, "lamports", u64); let interval = Duration::from_secs(value_t_or_exit!(matches, "interval", u64)); let count = if matches.is_present("count") { @@ -242,7 +250,12 @@ pub fn parse_cluster_ping(matches: &ArgMatches<'_>) -> Result) -> Result) -> Result) -> Result) -> Result) -> Result) -> Result) -> Result ProcessResult { 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!(); @@ -714,11 +727,13 @@ pub fn process_ping( let (recent_blockhash, fee_calculator) = rpc_client.get_new_blockhash(&last_blockhash)?; last_blockhash = recent_blockhash; - let transaction = - system_transaction::transfer(&config.keypair, &to, lamports, recent_blockhash); + let ix = system_instruction::transfer(&config.signers[0].pubkey(), &to, lamports); + let message = Message::new(vec![ix]); + let mut transaction = Transaction::new_unsigned(message); + transaction.try_sign(&config.signers, recent_blockhash)?; check_account_for_fee( rpc_client, - &config.keypair.pubkey(), + &config.signers[0].pubkey(), &fee_calculator, &transaction.message, )?; @@ -1015,28 +1030,38 @@ pub fn process_show_validators(rpc_client: &RpcClient, use_lamports_unit: bool) mod tests { use super::*; 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] fn test_parse_command() { 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 .clone() .get_matches_from(vec!["test", "cluster-version"]); assert_eq!( - parse_command(&test_cluster_version).unwrap(), + parse_command(&test_cluster_version, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::ClusterVersion, - require_keypair: false + signers: vec![], } ); let test_fees = test_commands.clone().get_matches_from(vec!["test", "fees"]); assert_eq!( - parse_command(&test_fees).unwrap(), + parse_command(&test_fees, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::Fees, - require_keypair: false + signers: vec![], } ); @@ -1046,10 +1071,10 @@ mod tests { .clone() .get_matches_from(vec!["test", "block-time", &slot.to_string()]); assert_eq!( - parse_command(&test_get_block_time).unwrap(), + parse_command(&test_get_block_time, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::GetBlockTime { slot }, - require_keypair: false + signers: vec![], } ); @@ -1057,12 +1082,12 @@ mod tests { .clone() .get_matches_from(vec!["test", "epoch-info"]); assert_eq!( - parse_command(&test_get_epoch_info).unwrap(), + parse_command(&test_get_epoch_info, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::GetEpochInfo { commitment_config: CommitmentConfig::recent(), }, - require_keypair: false + signers: vec![], } ); @@ -1070,21 +1095,21 @@ mod tests { .clone() .get_matches_from(vec!["test", "genesis-hash"]); assert_eq!( - parse_command(&test_get_genesis_hash).unwrap(), + parse_command(&test_get_genesis_hash, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::GetGenesisHash, - require_keypair: false + signers: vec![], } ); let test_get_slot = test_commands.clone().get_matches_from(vec!["test", "slot"]); assert_eq!( - parse_command(&test_get_slot).unwrap(), + parse_command(&test_get_slot, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::GetSlot { commitment_config: CommitmentConfig::recent(), }, - require_keypair: false + signers: vec![], } ); @@ -1092,12 +1117,12 @@ mod tests { .clone() .get_matches_from(vec!["test", "transaction-count"]); assert_eq!( - parse_command(&test_transaction_count).unwrap(), + parse_command(&test_transaction_count, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::GetTransactionCount { commitment_config: CommitmentConfig::recent(), }, - require_keypair: false + signers: vec![], } ); @@ -1113,7 +1138,7 @@ mod tests { "--confirmed", ]); assert_eq!( - parse_command(&test_ping).unwrap(), + parse_command(&test_ping, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::Ping { lamports: 1, @@ -1122,7 +1147,7 @@ mod tests { timeout: Duration::from_secs(3), commitment_config: CommitmentConfig::default(), }, - require_keypair: true + signers: vec![default_keypair.into()], } ); } diff --git a/cli/src/main.rs b/cli/src/main.rs index fd5e99e309..2131b38f3a 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -4,19 +4,15 @@ use console::style; use solana_clap_utils::{ input_parsers::derivation_of, input_validators::{is_derivation, is_url}, - keypair::{ - self, keypair_input, KeypairWithSource, ASK_SEED_PHRASE_ARG, - SKIP_SEED_PHRASE_VALIDATION_ARG, - }, + keypair::SKIP_SEED_PHRASE_VALIDATION_ARG, }; 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}, }; use solana_cli_config::config::{Config, CONFIG_FILE}; -use solana_sdk::signature::read_keypair_file; - -use std::error; +use solana_remote_wallet::remote_wallet::{maybe_wallet_manager, RemoteWalletManager}; +use std::{error, sync::Arc}; fn parse_settings(matches: &ArgMatches<'_>) -> Result> { let parse_args = match matches.subcommand() { @@ -84,7 +80,10 @@ fn parse_settings(matches: &ArgMatches<'_>) -> Result) -> Result> { +pub fn parse_args<'a>( + matches: &ArgMatches<'_>, + wallet_manager: Option>, +) -> Result<(CliConfig<'a>, CliSigners), Box> { let config = if let Some(config_file) = matches.value_of("config_file") { Config::load(config_file).unwrap_or_default() } else { @@ -99,62 +98,29 @@ pub fn parse_args(matches: &ArgMatches<'_>) -> Result ( - 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 - } else { - let 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://") { - keypair - } 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)) - } - } + let default_signer_path = if matches.is_present("keypair") { + matches.value_of("keypair").unwrap().to_string() + } else if config.keypair_path != "" { + config.keypair_path } else { - let default = CliConfig::default(); - (default.keypair, None) + CliConfig::default_keypair_path() }; - Ok(CliConfig { - command, - json_rpc_url, - keypair, - keypair_path, - derivation_path: derivation_of(matches, "derivation_path"), - rpc_client: None, - verbose: matches.is_present("verbose"), - }) + let CliCommandInfo { command, signers } = + parse_command(&matches, &default_signer_path, wallet_manager.as_ref())?; + + Ok(( + CliConfig { + command, + json_rpc_url, + signers: vec![], + keypair_path: default_signer_path, + derivation_path: derivation_of(matches, "derivation_path"), + rpc_client: None, + verbose: matches.is_present("verbose"), + }, + signers, + )) } fn main() -> Result<(), Box> { @@ -201,6 +167,7 @@ fn main() -> Result<(), Box> { Arg::with_name("derivation_path") .long("derivation-path") .value_name("ACCOUNT or ACCOUNT/CHANGE") + .global(true) .takes_value(true) .validator(is_derivation) .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> { .global(true) .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::with_name(SKIP_SEED_PHRASE_VALIDATION_ARG.name) .long(SKIP_SEED_PHRASE_VALIDATION_ARG.long) @@ -258,7 +216,10 @@ fn main() -> Result<(), Box> { .get_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)?; println!("{}", result); } diff --git a/cli/src/nonce.rs b/cli/src/nonce.rs index 2b95cb3047..6927560bf2 100644 --- a/cli/src/nonce.rs +++ b/cli/src/nonce.rs @@ -1,19 +1,21 @@ 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, - SigningAuthority, + SignerIndex, }; -use crate::offline::BLOCKHASH_ARG; 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_remote_wallet::remote_wallet::RemoteWalletManager; use solana_sdk::{ account::Account, account_utils::StateMut, hash::Hash, + message::Message, nonce_state::{Meta, NonceState}, pubkey::Pubkey, - signature::{Keypair, Signer}, system_instruction::{ advance_nonce_account, authorize_nonce_account, create_address_with_seed, create_nonce_account, create_nonce_account_with_seed, withdraw_nonce_account, NonceError, @@ -22,6 +24,7 @@ use solana_sdk::{ system_program, transaction::Transaction, }; +use std::sync::Arc; #[derive(Debug, Clone, PartialEq)] pub enum CliNonceError { @@ -65,8 +68,8 @@ pub fn nonce_authority_arg<'a, 'b>() -> Arg<'a, 'b> { Arg::with_name(NONCE_AUTHORITY_ARG.name) .long(NONCE_AUTHORITY_ARG.long) .takes_value(true) - .value_name("KEYPAIR or PUBKEY") - .validator(is_pubkey_or_keypair_or_ask_keyword) + .value_name("KEYPAIR or PUBKEY or REMOTE WALLET PATH") + .validator(is_valid_signer) .help(NONCE_AUTHORITY_ARG.help) } @@ -215,36 +218,61 @@ impl NonceSubCommands for App<'_, '_> { } } -pub fn parse_authorize_nonce_account(matches: &ArgMatches<'_>) -> Result { +pub fn parse_authorize_nonce_account( + matches: &ArgMatches<'_>, + default_signer_path: &str, + wallet_manager: Option<&Arc>, +) -> Result { let nonce_account = pubkey_of(matches, "nonce_account_keypair").unwrap(); let new_authority = pubkey_of(matches, "new_authority").unwrap(); - let nonce_authority = - SigningAuthority::new_from_matches(&matches, NONCE_AUTHORITY_ARG.name, None)?; + let (nonce_authority, nonce_authority_pubkey) = + 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 { command: CliCommand::AuthorizeNonceAccount { nonce_account, - nonce_authority, + nonce_authority: signer_info.index_of(nonce_authority_pubkey).unwrap(), new_authority, }, - require_keypair: true, + signers: signer_info.signers, }) } -pub fn parse_nonce_create_account(matches: &ArgMatches<'_>) -> Result { - let nonce_account = keypair_of(matches, "nonce_account_keypair").unwrap(); +pub fn parse_nonce_create_account( + matches: &ArgMatches<'_>, + default_signer_path: &str, + wallet_manager: Option<&Arc>, +) -> Result { + 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 lamports = lamports_of_sol(matches, "amount").unwrap(); 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 { command: CliCommand::CreateNonceAccount { - nonce_account: nonce_account.into(), + nonce_account: signer_info.index_of(nonce_account_pubkey).unwrap(), seed, nonce_authority, lamports, }, - require_keypair: true, + signers: signer_info.signers, }) } @@ -253,21 +281,33 @@ pub fn parse_get_nonce(matches: &ArgMatches<'_>) -> Result) -> Result { +pub fn parse_new_nonce( + matches: &ArgMatches<'_>, + default_signer_path: &str, + wallet_manager: Option<&Arc>, +) -> Result { let nonce_account = pubkey_of(matches, "nonce_account_keypair").unwrap(); - let nonce_authority = - SigningAuthority::new_from_matches(&matches, NONCE_AUTHORITY_ARG.name, None)?; + let (nonce_authority, nonce_authority_pubkey) = + 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 { command: CliCommand::NewNonce { 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, + default_signer_path: &str, + wallet_manager: Option<&Arc>, ) -> Result { let nonce_account = pubkey_of(matches, "nonce_account_keypair").unwrap(); let destination_account_pubkey = pubkey_of(matches, "destination_account_pubkey").unwrap(); let lamports = lamports_of_sol(matches, "amount").unwrap(); - let nonce_authority = - SigningAuthority::new_from_matches(&matches, NONCE_AUTHORITY_ARG.name, None)?; + let (nonce_authority, nonce_authority_pubkey) = + 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 { command: CliCommand::WithdrawFromNonceAccount { nonce_account, - nonce_authority, + nonce_authority: signer_info.index_of(nonce_authority_pubkey).unwrap(), destination_account_pubkey, lamports, }, - require_keypair: true, + signers: signer_info.signers, }) } @@ -336,41 +386,36 @@ pub fn process_authorize_nonce_account( rpc_client: &RpcClient, config: &CliConfig, nonce_account: &Pubkey, - nonce_authority: Option<&SigningAuthority>, + nonce_authority: SignerIndex, new_authority: &Pubkey, ) -> ProcessResult { let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; - let nonce_authority = nonce_authority - .map(|a| a.keypair()) - .unwrap_or(&config.keypair); + let nonce_authority = config.signers[nonce_authority]; let ix = authorize_nonce_account(nonce_account, &nonce_authority.pubkey(), new_authority); - let mut tx = Transaction::new_signed_with_payer( - vec![ix], - Some(&config.keypair.pubkey()), - &[&config.keypair, nonce_authority], - recent_blockhash, - ); + let message = Message::new_with_payer(vec![ix], Some(&config.signers[0].pubkey())); + let mut tx = Transaction::new_unsigned(message); + tx.try_sign(&config.signers, recent_blockhash)?; + check_account_for_fee( rpc_client, - &config.keypair.pubkey(), + &config.signers[0].pubkey(), &fee_calculator, &tx.message, )?; - let result = - rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair, nonce_authority]); + let result = rpc_client.send_and_confirm_transaction(&mut tx, &config.signers); log_instruction_custom_error::(result) } pub fn process_create_nonce_account( rpc_client: &RpcClient, config: &CliConfig, - nonce_account: &Keypair, + nonce_account: SignerIndex, seed: Option, nonce_authority: Option, lamports: u64, ) -> 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() { create_address_with_seed(&nonce_account_pubkey, &seed, &system_program::id())? } else { @@ -378,7 +423,7 @@ pub fn process_create_nonce_account( }; 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()), )?; @@ -405,20 +450,20 @@ pub fn process_create_nonce_account( .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 { create_nonce_account_with_seed( - &config.keypair.pubkey(), // from - &nonce_account_address, // to - &nonce_account_pubkey, // base - &seed, // seed + &config.signers[0].pubkey(), // from + &nonce_account_address, // to + &nonce_account_pubkey, // base + &seed, // seed &nonce_authority, lamports, ) } else { create_nonce_account( - &config.keypair.pubkey(), + &config.signers[0].pubkey(), &nonce_account_pubkey, &nonce_authority, lamports, @@ -427,25 +472,17 @@ pub fn process_create_nonce_account( let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; - let signers = if nonce_account_pubkey != config.keypair.pubkey() { - vec![&config.keypair, nonce_account] // both must sign if `from` and `to` differ - } else { - vec![&config.keypair] // when stake_account == config.keypair and there's a seed, we only need one signature - }; + let message = Message::new_with_payer(ixs, Some(&config.signers[0].pubkey())); + let mut tx = Transaction::new_unsigned(message); + tx.try_sign(&config.signers, recent_blockhash)?; - let mut tx = Transaction::new_signed_with_payer( - ixs, - Some(&config.keypair.pubkey()), - &signers, - recent_blockhash, - ); check_account_for_fee( rpc_client, - &config.keypair.pubkey(), + &config.signers[0].pubkey(), &fee_calculator, &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::(result) } @@ -473,10 +510,10 @@ pub fn process_new_nonce( rpc_client: &RpcClient, config: &CliConfig, nonce_account: &Pubkey, - nonce_authority: Option<&SigningAuthority>, + nonce_authority: SignerIndex, ) -> ProcessResult { 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()), )?; @@ -487,25 +524,20 @@ pub fn process_new_nonce( .into()); } - let nonce_authority = nonce_authority - .map(|a| a.keypair()) - .unwrap_or(&config.keypair); + let nonce_authority = config.signers[nonce_authority]; let ix = advance_nonce_account(&nonce_account, &nonce_authority.pubkey()); let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; - let mut tx = Transaction::new_signed_with_payer( - vec![ix], - Some(&config.keypair.pubkey()), - &[&config.keypair, nonce_authority], - recent_blockhash, - ); + let message = Message::new_with_payer(vec![ix], Some(&config.signers[0].pubkey())); + let mut tx = Transaction::new_unsigned(message); + tx.try_sign(&config.signers, recent_blockhash)?; check_account_for_fee( rpc_client, - &config.keypair.pubkey(), + &config.signers[0].pubkey(), &fee_calculator, &tx.message, )?; 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::(result) } @@ -562,35 +594,29 @@ pub fn process_withdraw_from_nonce_account( rpc_client: &RpcClient, config: &CliConfig, nonce_account: &Pubkey, - nonce_authority: Option<&SigningAuthority>, + nonce_authority: SignerIndex, destination_account_pubkey: &Pubkey, lamports: u64, ) -> ProcessResult { let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; - let nonce_authority = nonce_authority - .map(|a| a.keypair()) - .unwrap_or(&config.keypair); + let nonce_authority = config.signers[nonce_authority]; let ix = withdraw_nonce_account( nonce_account, &nonce_authority.pubkey(), destination_account_pubkey, lamports, ); - let mut tx = Transaction::new_signed_with_payer( - vec![ix], - Some(&config.keypair.pubkey()), - &[&config.keypair, nonce_authority], - recent_blockhash, - ); + let message = Message::new_with_payer(vec![ix], Some(&config.signers[0].pubkey())); + let mut tx = Transaction::new_unsigned(message); + tx.try_sign(&config.signers, recent_blockhash)?; check_account_for_fee( rpc_client, - &config.keypair.pubkey(), + &config.signers[0].pubkey(), &fee_calculator, &tx.message, )?; - let result = - rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair, nonce_authority]); + let result = rpc_client.send_and_confirm_transaction(&mut tx, &config.signers); log_instruction_custom_error::(result) } @@ -602,7 +628,7 @@ mod tests { account::Account, hash::hash, nonce_state::{Meta as NonceMeta, NonceState}, - signature::{read_keypair_file, write_keypair}, + signature::{read_keypair_file, write_keypair, Keypair, Signer}, system_program, }; use tempfile::NamedTempFile; @@ -615,6 +641,9 @@ mod tests { #[test] fn test_parse_command() { 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 nonce_account_keypair = Keypair::new(); write_keypair(&nonce_account_keypair, tmp_file.as_file_mut()).unwrap(); @@ -633,14 +662,14 @@ mod tests { &Pubkey::default().to_string(), ]); assert_eq!( - parse_command(&test_authorize_nonce_account).unwrap(), + parse_command(&test_authorize_nonce_account, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::AuthorizeNonceAccount { nonce_account: nonce_account_pubkey, - nonce_authority: None, + nonce_authority: 0, 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, ]); assert_eq!( - parse_command(&test_authorize_nonce_account).unwrap(), + parse_command(&test_authorize_nonce_account, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::AuthorizeNonceAccount { nonce_account: read_keypair_file(&keypair_file).unwrap().pubkey(), - nonce_authority: Some( - read_keypair_file(&authority_keypair_file).unwrap().into() - ), + nonce_authority: 1, 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", ]); assert_eq!( - parse_command(&test_create_nonce_account).unwrap(), + parse_command(&test_create_nonce_account, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::CreateNonceAccount { - nonce_account: read_keypair_file(&keypair_file).unwrap().into(), + nonce_account: 1, seed: None, nonce_authority: None, 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, ]); assert_eq!( - parse_command(&test_create_nonce_account).unwrap(), + parse_command(&test_create_nonce_account, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::CreateNonceAccount { - nonce_account: read_keypair_file(&keypair_file).unwrap().into(), + nonce_account: 1, seed: None, - nonce_authority: Some( - read_keypair_file(&authority_keypair_file).unwrap().pubkey() - ), + nonce_authority: Some(nonce_authority_keypair.pubkey()), 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, ]); assert_eq!( - parse_command(&test_get_nonce).unwrap(), + parse_command(&test_get_nonce, &default_keypair_file, None).unwrap(), CliCommandInfo { - command: CliCommand::GetNonce(nonce_account_keypair.pubkey(),), - require_keypair: false + command: CliCommand::GetNonce(nonce_account_keypair.pubkey()), + signers: vec![], } ); @@ -732,13 +766,13 @@ mod tests { .get_matches_from(vec!["test", "new-nonce", &keypair_file]); let nonce_account = read_keypair_file(&keypair_file).unwrap(); assert_eq!( - parse_command(&test_new_nonce).unwrap(), + parse_command(&test_new_nonce, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::NewNonce { 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(); assert_eq!( - parse_command(&test_new_nonce).unwrap(), + parse_command(&test_new_nonce, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::NewNonce { nonce_account: nonce_account.pubkey(), - nonce_authority: Some( - read_keypair_file(&authority_keypair_file).unwrap().into() - ), + nonce_authority: 1, }, - 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, ]); assert_eq!( - parse_command(&test_show_nonce_account).unwrap(), + parse_command(&test_show_nonce_account, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::ShowNonceAccount { nonce_account_pubkey: nonce_account_keypair.pubkey(), use_lamports_unit: false, }, - require_keypair: false + signers: vec![], } ); @@ -790,15 +825,20 @@ mod tests { "42", ]); assert_eq!( - parse_command(&test_withdraw_from_nonce_account).unwrap(), + parse_command( + &test_withdraw_from_nonce_account, + &default_keypair_file, + None + ) + .unwrap(), CliCommandInfo { command: CliCommand::WithdrawFromNonceAccount { nonce_account: read_keypair_file(&keypair_file).unwrap().pubkey(), - nonce_authority: None, + nonce_authority: 0, destination_account_pubkey: nonce_account_pubkey, 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", ]); assert_eq!( - parse_command(&test_withdraw_from_nonce_account).unwrap(), + parse_command( + &test_withdraw_from_nonce_account, + &default_keypair_file, + None + ) + .unwrap(), CliCommandInfo { command: CliCommand::WithdrawFromNonceAccount { nonce_account: read_keypair_file(&keypair_file).unwrap().pubkey(), - nonce_authority: None, + nonce_authority: 0, destination_account_pubkey: nonce_account_pubkey, lamports: 42000000000 }, - require_keypair: true + signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()], } ); @@ -833,17 +878,23 @@ mod tests { &authority_keypair_file, ]); assert_eq!( - parse_command(&test_withdraw_from_nonce_account).unwrap(), + parse_command( + &test_withdraw_from_nonce_account, + &default_keypair_file, + None + ) + .unwrap(), CliCommandInfo { command: CliCommand::WithdrawFromNonceAccount { nonce_account: read_keypair_file(&keypair_file).unwrap().pubkey(), - nonce_authority: Some( - read_keypair_file(&authority_keypair_file).unwrap().into() - ), + nonce_authority: 1, destination_account_pubkey: nonce_account_pubkey, 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() + ], } ); } diff --git a/cli/src/offline.rs b/cli/src/offline.rs index 2818c84b57..5aee7142bb 100644 --- a/cli/src/offline.rs +++ b/cli/src/offline.rs @@ -3,30 +3,12 @@ use serde_json::Value; use solana_clap_utils::{ input_parsers::value_of, input_validators::{is_hash, is_pubkey_sig}, - ArgConstant, + offline::{BLOCKHASH_ARG, SIGNER_ARG, SIGN_ONLY_ARG}, }; use solana_client::rpc_client::RpcClient; use solana_sdk::{fee_calculator::FeeCalculator, hash::Hash, pubkey::Pubkey, signature::Signature}; 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)] pub enum BlockhashQuery { None(Hash, FeeCalculator), diff --git a/cli/src/stake.rs b/cli/src/stake.rs index 6efd74ec5f..e6cf47b445 100644 --- a/cli/src/stake.rs +++ b/cli/src/stake.rs @@ -1,22 +1,21 @@ use crate::{ cli::{ build_balance_message, check_account_for_fee, check_unique_pubkeys, fee_payer_arg, - log_instruction_custom_error, nonce_authority_arg, replace_signatures, return_signers, - CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult, SigningAuthority, - FEE_PAYER_ARG, + generate_unique_signers, log_instruction_custom_error, nonce_authority_arg, return_signers, + CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult, SignerIndex, FEE_PAYER_ARG, }, nonce::{check_nonce_account, nonce_arg, NONCE_ARG, NONCE_AUTHORITY_ARG}, offline::*, }; use clap::{App, Arg, ArgMatches, SubCommand}; use console::style; -use solana_clap_utils::{input_parsers::*, input_validators::*, ArgConstant}; +use solana_clap_utils::{input_parsers::*, input_validators::*, offline::*, ArgConstant}; use solana_client::rpc_client::RpcClient; -use solana_sdk::signature::{Keypair, Signature}; +use solana_remote_wallet::remote_wallet::RemoteWalletManager; use solana_sdk::{ account_utils::StateMut, + message::Message, pubkey::Pubkey, - signature::Signer, system_instruction::{create_address_with_seed, SystemError}, sysvar::{ stake_history::{self, StakeHistory}, @@ -29,7 +28,7 @@ use solana_stake_program::{ stake_state::{Authorized, Lockup, Meta, StakeAuthorize, StakeState}, }; use solana_vote_program::vote_state::VoteState; -use std::ops::Deref; +use std::{ops::Deref, sync::Arc}; pub const STAKE_AUTHORITY_ARG: ArgConstant<'static> = ArgConstant { name: "stake_authority", @@ -47,8 +46,8 @@ fn stake_authority_arg<'a, 'b>() -> Arg<'a, 'b> { Arg::with_name(STAKE_AUTHORITY_ARG.name) .long(STAKE_AUTHORITY_ARG.long) .takes_value(true) - .value_name("KEYPAIR or PUBKEY") - .validator(is_pubkey_or_keypair_or_ask_keyword) + .value_name("KEYPAIR or PUBKEY or REMOTE WALLET PATH") + .validator(is_valid_signer) .help(STAKE_AUTHORITY_ARG.help) } @@ -56,8 +55,8 @@ fn withdraw_authority_arg<'a, 'b>() -> Arg<'a, 'b> { Arg::with_name(WITHDRAW_AUTHORITY_ARG.name) .long(WITHDRAW_AUTHORITY_ARG.long) .takes_value(true) - .value_name("KEYPAIR or PUBKEY") - .validator(is_pubkey_or_keypair_or_ask_keyword) + .value_name("KEYPAIR or PUBKEY or REMOTE WALLET PATH") + .validator(is_valid_signer) .help(WITHDRAW_AUTHORITY_ARG.help) } @@ -76,7 +75,7 @@ impl StakeSubCommands for App<'_, '_> { .value_name("STAKE ACCOUNT") .takes_value(true) .required(true) - .validator(is_pubkey_or_keypair_or_ask_keyword) + .validator(is_valid_signer) .help("Signing authority of the stake address to fund") ) .arg( @@ -138,8 +137,8 @@ impl StakeSubCommands for App<'_, '_> { Arg::with_name("from") .long("from") .takes_value(true) - .value_name("KEYPAIR or PUBKEY") - .validator(is_pubkey_or_keypair_or_ask_keyword) + .value_name("KEYPAIR or PUBKEY or REMOTE WALLET PATH") + .validator(is_valid_signer) .help("Source account of funds (if different from client local account)"), ) .offline_args() @@ -369,8 +368,8 @@ impl StakeSubCommands for App<'_, '_> { Arg::with_name("custodian") .long("custodian") .takes_value(true) - .value_name("KEYPAIR or PUBKEY") - .validator(is_pubkey_or_keypair_or_ask_keyword) + .value_name("KEYPAIR or PUBKEY or REMOTE WALLET PATH") + .validator(is_valid_signer) .help("Public key of signing custodian (defaults to cli config pubkey)") ) .offline_args() @@ -412,32 +411,38 @@ impl StakeSubCommands for App<'_, '_> { } } -pub fn parse_stake_create_account(matches: &ArgMatches<'_>) -> Result { +pub fn parse_stake_create_account( + matches: &ArgMatches<'_>, + default_signer_path: &str, + wallet_manager: Option<&Arc>, +) -> Result { let seed = matches.value_of("seed").map(|s| s.to_string()); - let epoch = value_of(&matches, "lockup_epoch").unwrap_or(0); - let unix_timestamp = unix_timestamp_from_rfc3339_datetime(&matches, "lockup_date").unwrap_or(0); + let epoch = value_of(matches, "lockup_epoch").unwrap_or(0); + let unix_timestamp = unix_timestamp_from_rfc3339_datetime(matches, "lockup_date").unwrap_or(0); let custodian = pubkey_of(matches, "custodian").unwrap_or_default(); let staker = pubkey_of(matches, STAKE_AUTHORITY_ARG.name); let withdrawer = pubkey_of(matches, WITHDRAW_AUTHORITY_ARG.name); let lamports = lamports_of_sol(matches, "amount").unwrap(); let sign_only = matches.is_present(SIGN_ONLY_ARG.name); - let signers = pubkeys_sigs_of(&matches, SIGNER_ARG.name); let blockhash_query = BlockhashQuery::new_from_matches(matches); - let require_keypair = signers.is_none(); - let nonce_account = pubkey_of(&matches, NONCE_ARG.name); - let nonce_authority = - SigningAuthority::new_from_matches(&matches, NONCE_AUTHORITY_ARG.name, signers.as_deref())?; - let fee_payer = - SigningAuthority::new_from_matches(&matches, FEE_PAYER_ARG.name, signers.as_deref())?; - let from = SigningAuthority::new_from_matches(&matches, "from", signers.as_deref())?; - let stake_account = - SigningAuthority::new_from_matches(&matches, "stake_account", signers.as_deref()) - .unwrap() - .unwrap(); + let nonce_account = pubkey_of(matches, NONCE_ARG.name); + let (nonce_authority, nonce_authority_pubkey) = + signer_of(matches, NONCE_AUTHORITY_ARG.name, wallet_manager)?; + let (fee_payer, fee_payer_pubkey) = signer_of(matches, FEE_PAYER_ARG.name, wallet_manager)?; + let (from, from_pubkey) = signer_of(matches, "from", wallet_manager)?; + let (stake_account, stake_account_pubkey) = + signer_of(matches, "stake_account", wallet_manager)?; + + let mut bulk_signers = vec![fee_payer, from, stake_account]; + if nonce_account.is_some() { + bulk_signers.push(nonce_authority); + } + let signer_info = + generate_unique_signers(bulk_signers, matches, default_signer_path, wallet_manager)?; Ok(CliCommandInfo { command: CliCommand::CreateStakeAccount { - stake_account, + stake_account: signer_info.index_of(stake_account_pubkey).unwrap(), seed, staker, withdrawer, @@ -448,52 +453,60 @@ pub fn parse_stake_create_account(matches: &ArgMatches<'_>) -> Result) -> Result { +pub fn parse_stake_delegate_stake( + matches: &ArgMatches<'_>, + default_signer_path: &str, + wallet_manager: Option<&Arc>, +) -> Result { let stake_account_pubkey = pubkey_of(matches, "stake_account_pubkey").unwrap(); let vote_account_pubkey = pubkey_of(matches, "vote_account_pubkey").unwrap(); let force = matches.is_present("force"); let sign_only = matches.is_present(SIGN_ONLY_ARG.name); - let signers = pubkeys_sigs_of(&matches, SIGNER_ARG.name); let blockhash_query = BlockhashQuery::new_from_matches(matches); - let require_keypair = signers.is_none(); - let nonce_account = pubkey_of(&matches, NONCE_ARG.name); - let stake_authority = - SigningAuthority::new_from_matches(&matches, STAKE_AUTHORITY_ARG.name, signers.as_deref())?; - let nonce_authority = - SigningAuthority::new_from_matches(&matches, NONCE_AUTHORITY_ARG.name, signers.as_deref())?; - let fee_payer = - SigningAuthority::new_from_matches(&matches, FEE_PAYER_ARG.name, signers.as_deref())?; + let nonce_account = pubkey_of(matches, NONCE_ARG.name); + let (stake_authority, stake_authority_pubkey) = + signer_of(matches, STAKE_AUTHORITY_ARG.name, wallet_manager)?; + let (nonce_authority, nonce_authority_pubkey) = + signer_of(matches, NONCE_AUTHORITY_ARG.name, wallet_manager)?; + let (fee_payer, fee_payer_pubkey) = signer_of(matches, FEE_PAYER_ARG.name, wallet_manager)?; + + let mut bulk_signers = vec![stake_authority, fee_payer]; + if nonce_account.is_some() { + bulk_signers.push(nonce_authority); + } + let signer_info = + generate_unique_signers(bulk_signers, matches, default_signer_path, wallet_manager)?; Ok(CliCommandInfo { command: CliCommand::DelegateStake { stake_account_pubkey, vote_account_pubkey, - stake_authority, + stake_authority: signer_info.index_of(stake_authority_pubkey).unwrap(), force, sign_only, - signers, blockhash_query, nonce_account, - nonce_authority, - fee_payer, + nonce_authority: signer_info.index_of(nonce_authority_pubkey).unwrap(), + fee_payer: signer_info.index_of(fee_payer_pubkey).unwrap(), }, - require_keypair, + signers: signer_info.signers, }) } pub fn parse_stake_authorize( matches: &ArgMatches<'_>, + default_signer_path: &str, + wallet_manager: Option<&Arc>, stake_authorize: StakeAuthorize, ) -> Result { let stake_account_pubkey = pubkey_of(matches, "stake_account_pubkey").unwrap(); @@ -503,151 +516,181 @@ pub fn parse_stake_authorize( StakeAuthorize::Withdrawer => WITHDRAW_AUTHORITY_ARG.name, }; let sign_only = matches.is_present(SIGN_ONLY_ARG.name); - let signers = pubkeys_sigs_of(&matches, SIGNER_ARG.name); - let authority = - SigningAuthority::new_from_matches(&matches, authority_flag, signers.as_deref())?; + let (authority, authority_pubkey) = signer_of(matches, authority_flag, wallet_manager)?; let blockhash_query = BlockhashQuery::new_from_matches(matches); - let nonce_account = pubkey_of(&matches, NONCE_ARG.name); - let nonce_authority = - SigningAuthority::new_from_matches(&matches, NONCE_AUTHORITY_ARG.name, signers.as_deref())?; - let fee_payer = - SigningAuthority::new_from_matches(&matches, FEE_PAYER_ARG.name, signers.as_deref())?; + let nonce_account = pubkey_of(matches, NONCE_ARG.name); + let (nonce_authority, nonce_authority_pubkey) = + signer_of(matches, NONCE_AUTHORITY_ARG.name, wallet_manager)?; + let (fee_payer, fee_payer_pubkey) = signer_of(matches, FEE_PAYER_ARG.name, wallet_manager)?; + + let mut bulk_signers = vec![authority, fee_payer]; + if nonce_account.is_some() { + bulk_signers.push(nonce_authority); + } + let signer_info = + generate_unique_signers(bulk_signers, matches, default_signer_path, wallet_manager)?; Ok(CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, new_authorized_pubkey, stake_authorize, - authority, + authority: signer_info.index_of(authority_pubkey).unwrap(), sign_only, - signers, blockhash_query, nonce_account, - nonce_authority, - fee_payer, + nonce_authority: signer_info.index_of(nonce_authority_pubkey).unwrap(), + fee_payer: signer_info.index_of(fee_payer_pubkey).unwrap(), }, - require_keypair: true, + signers: signer_info.signers, }) } -pub fn parse_split_stake(matches: &ArgMatches<'_>) -> Result { +pub fn parse_split_stake( + matches: &ArgMatches<'_>, + default_signer_path: &str, + wallet_manager: Option<&Arc>, +) -> Result { let stake_account_pubkey = pubkey_of(matches, "stake_account_pubkey").unwrap(); - let split_stake_account = keypair_of(matches, "split_stake_account").unwrap(); + let (split_stake_account, split_stake_account_pubkey) = + signer_of(matches, "split_stake_account", wallet_manager)?; let lamports = lamports_of_sol(matches, "amount").unwrap(); let seed = matches.value_of("seed").map(|s| s.to_string()); let sign_only = matches.is_present(SIGN_ONLY_ARG.name); - let signers = pubkeys_sigs_of(&matches, SIGNER_ARG.name); let blockhash_query = BlockhashQuery::new_from_matches(matches); - let require_keypair = signers.is_none(); - let nonce_account = pubkey_of(&matches, NONCE_ARG.name); - let stake_authority = - SigningAuthority::new_from_matches(&matches, STAKE_AUTHORITY_ARG.name, signers.as_deref())?; - let nonce_authority = - SigningAuthority::new_from_matches(&matches, NONCE_AUTHORITY_ARG.name, signers.as_deref())?; - let fee_payer = - SigningAuthority::new_from_matches(&matches, FEE_PAYER_ARG.name, signers.as_deref())?; + let nonce_account = pubkey_of(matches, NONCE_ARG.name); + let (stake_authority, stake_authority_pubkey) = + signer_of(matches, STAKE_AUTHORITY_ARG.name, wallet_manager)?; + let (nonce_authority, nonce_authority_pubkey) = + signer_of(matches, NONCE_AUTHORITY_ARG.name, wallet_manager)?; + let (fee_payer, fee_payer_pubkey) = signer_of(matches, FEE_PAYER_ARG.name, wallet_manager)?; + + let mut bulk_signers = vec![stake_authority, fee_payer, split_stake_account]; + if nonce_account.is_some() { + bulk_signers.push(nonce_authority); + } + let signer_info = + generate_unique_signers(bulk_signers, matches, default_signer_path, wallet_manager)?; Ok(CliCommandInfo { command: CliCommand::SplitStake { stake_account_pubkey, - stake_authority, + stake_authority: signer_info.index_of(stake_authority_pubkey).unwrap(), sign_only, - signers, blockhash_query, nonce_account, - nonce_authority, - split_stake_account: split_stake_account.into(), + nonce_authority: signer_info.index_of(nonce_authority_pubkey).unwrap(), + split_stake_account: signer_info.index_of(split_stake_account_pubkey).unwrap(), seed, lamports, - fee_payer, + fee_payer: signer_info.index_of(fee_payer_pubkey).unwrap(), }, - require_keypair, + signers: signer_info.signers, }) } -pub fn parse_stake_deactivate_stake(matches: &ArgMatches<'_>) -> Result { +pub fn parse_stake_deactivate_stake( + matches: &ArgMatches<'_>, + default_signer_path: &str, + wallet_manager: Option<&Arc>, +) -> Result { let stake_account_pubkey = pubkey_of(matches, "stake_account_pubkey").unwrap(); let sign_only = matches.is_present(SIGN_ONLY_ARG.name); - let signers = pubkeys_sigs_of(&matches, SIGNER_ARG.name); let blockhash_query = BlockhashQuery::new_from_matches(matches); - let require_keypair = signers.is_none(); - let nonce_account = pubkey_of(&matches, NONCE_ARG.name); - let stake_authority = - SigningAuthority::new_from_matches(&matches, STAKE_AUTHORITY_ARG.name, signers.as_deref())?; - let nonce_authority = - SigningAuthority::new_from_matches(&matches, NONCE_AUTHORITY_ARG.name, signers.as_deref())?; - let fee_payer = - SigningAuthority::new_from_matches(&matches, FEE_PAYER_ARG.name, signers.as_deref())?; + let nonce_account = pubkey_of(matches, NONCE_ARG.name); + let (stake_authority, stake_authority_pubkey) = + signer_of(matches, STAKE_AUTHORITY_ARG.name, wallet_manager)?; + let (nonce_authority, nonce_authority_pubkey) = + signer_of(matches, NONCE_AUTHORITY_ARG.name, wallet_manager)?; + let (fee_payer, fee_payer_pubkey) = signer_of(matches, FEE_PAYER_ARG.name, wallet_manager)?; + + let mut bulk_signers = vec![stake_authority, fee_payer]; + if nonce_account.is_some() { + bulk_signers.push(nonce_authority); + } + let signer_info = + generate_unique_signers(bulk_signers, matches, default_signer_path, wallet_manager)?; Ok(CliCommandInfo { command: CliCommand::DeactivateStake { stake_account_pubkey, - stake_authority, + stake_authority: signer_info.index_of(stake_authority_pubkey).unwrap(), sign_only, - signers, blockhash_query, nonce_account, - nonce_authority, - fee_payer, + nonce_authority: signer_info.index_of(nonce_authority_pubkey).unwrap(), + fee_payer: signer_info.index_of(fee_payer_pubkey).unwrap(), }, - require_keypair, + signers: signer_info.signers, }) } -pub fn parse_stake_withdraw_stake(matches: &ArgMatches<'_>) -> Result { +pub fn parse_stake_withdraw_stake( + matches: &ArgMatches<'_>, + default_signer_path: &str, + wallet_manager: Option<&Arc>, +) -> Result { let stake_account_pubkey = pubkey_of(matches, "stake_account_pubkey").unwrap(); let destination_account_pubkey = pubkey_of(matches, "destination_account_pubkey").unwrap(); let lamports = lamports_of_sol(matches, "amount").unwrap(); let sign_only = matches.is_present(SIGN_ONLY_ARG.name); - let signers = pubkeys_sigs_of(&matches, SIGNER_ARG.name); let blockhash_query = BlockhashQuery::new_from_matches(matches); - let require_keypair = signers.is_none(); - let nonce_account = pubkey_of(&matches, NONCE_ARG.name); - let nonce_authority = - SigningAuthority::new_from_matches(&matches, NONCE_AUTHORITY_ARG.name, signers.as_deref())?; - let withdraw_authority = SigningAuthority::new_from_matches( - &matches, - WITHDRAW_AUTHORITY_ARG.name, - signers.as_deref(), - )?; - let fee_payer = - SigningAuthority::new_from_matches(&matches, FEE_PAYER_ARG.name, signers.as_deref())?; + let nonce_account = pubkey_of(matches, NONCE_ARG.name); + let (withdraw_authority, withdraw_authority_pubkey) = + signer_of(matches, WITHDRAW_AUTHORITY_ARG.name, wallet_manager)?; + let (nonce_authority, nonce_authority_pubkey) = + signer_of(matches, NONCE_AUTHORITY_ARG.name, wallet_manager)?; + let (fee_payer, fee_payer_pubkey) = signer_of(matches, FEE_PAYER_ARG.name, wallet_manager)?; + + let mut bulk_signers = vec![withdraw_authority, fee_payer]; + if nonce_account.is_some() { + bulk_signers.push(nonce_authority); + } + let signer_info = + generate_unique_signers(bulk_signers, matches, default_signer_path, wallet_manager)?; Ok(CliCommandInfo { command: CliCommand::WithdrawStake { stake_account_pubkey, destination_account_pubkey, lamports, - withdraw_authority, + withdraw_authority: signer_info.index_of(withdraw_authority_pubkey).unwrap(), sign_only, - signers, blockhash_query, nonce_account, - nonce_authority, - fee_payer, + nonce_authority: signer_info.index_of(nonce_authority_pubkey).unwrap(), + fee_payer: signer_info.index_of(fee_payer_pubkey).unwrap(), }, - require_keypair, + signers: signer_info.signers, }) } -pub fn parse_stake_set_lockup(matches: &ArgMatches<'_>) -> Result { +pub fn parse_stake_set_lockup( + matches: &ArgMatches<'_>, + default_signer_path: &str, + wallet_manager: Option<&Arc>, +) -> Result { let stake_account_pubkey = pubkey_of(matches, "stake_account_pubkey").unwrap(); - let epoch = value_of(&matches, "lockup_epoch").unwrap_or(0); - let unix_timestamp = unix_timestamp_from_rfc3339_datetime(&matches, "lockup_date").unwrap_or(0); + let epoch = value_of(matches, "lockup_epoch").unwrap_or(0); + let unix_timestamp = unix_timestamp_from_rfc3339_datetime(matches, "lockup_date").unwrap_or(0); let new_custodian = pubkey_of(matches, "new_custodian").unwrap_or_default(); let sign_only = matches.is_present(SIGN_ONLY_ARG.name); - let signers = pubkeys_sigs_of(&matches, SIGNER_ARG.name); let blockhash_query = BlockhashQuery::new_from_matches(matches); - let require_keypair = signers.is_none(); - let nonce_account = pubkey_of(&matches, NONCE_ARG.name); + let nonce_account = pubkey_of(matches, NONCE_ARG.name); - let custodian = SigningAuthority::new_from_matches(&matches, "custodian", signers.as_deref())?; - let nonce_authority = - SigningAuthority::new_from_matches(&matches, NONCE_AUTHORITY_ARG.name, signers.as_deref())?; - let fee_payer = - SigningAuthority::new_from_matches(&matches, FEE_PAYER_ARG.name, signers.as_deref())?; + let (custodian, custodian_pubkey) = signer_of(matches, "custodian", wallet_manager)?; + let (nonce_authority, nonce_authority_pubkey) = + signer_of(matches, NONCE_AUTHORITY_ARG.name, wallet_manager)?; + let (fee_payer, fee_payer_pubkey) = signer_of(matches, FEE_PAYER_ARG.name, wallet_manager)?; + + let mut bulk_signers = vec![custodian, fee_payer]; + if nonce_account.is_some() { + bulk_signers.push(nonce_authority); + } + let signer_info = + generate_unique_signers(bulk_signers, matches, default_signer_path, wallet_manager)?; Ok(CliCommandInfo { command: CliCommand::StakeSetLockup { @@ -657,15 +700,14 @@ pub fn parse_stake_set_lockup(matches: &ArgMatches<'_>) -> Result) -> Result) -> Result) -> Result, staker: &Option, withdrawer: &Option, lockup: &Lockup, lamports: u64, sign_only: bool, - signers: Option<&Vec<(Pubkey, Signature)>>, blockhash_query: &BlockhashQuery, nonce_account: Option<&Pubkey>, - nonce_authority: Option<&SigningAuthority>, - fee_payer: Option<&SigningAuthority>, - from: Option<&SigningAuthority>, + nonce_authority: SignerIndex, + fee_payer: SignerIndex, + from: SignerIndex, ) -> ProcessResult { - // Offline derived address creation currently is not possible - // https://github.com/solana-labs/solana/pull/8252 - if seed.is_some() && (sign_only || signers.is_some()) { - return Err(CliError::BadParameter( - "Offline stake account creation with derived addresses are not yet supported" - .to_string(), - ) - .into()); - } - - let (stake_account_pubkey, stake_account) = (stake_account.pubkey(), stake_account.keypair()); + let stake_account = config.signers[stake_account]; let stake_account_address = if let Some(seed) = seed { - create_address_with_seed(&stake_account_pubkey, &seed, &solana_stake_program::id())? + create_address_with_seed(&stake_account.pubkey(), &seed, &solana_stake_program::id())? } else { - stake_account_pubkey + stake_account.pubkey() }; - let (from_pubkey, from) = from - .map(|f| (f.pubkey(), f.keypair())) - .unwrap_or((config.keypair.pubkey(), &config.keypair)); + let from = config.signers[from]; check_unique_pubkeys( - (&from_pubkey, "from keypair".to_string()), + (&from.pubkey(), "from keypair".to_string()), (&stake_account_address, "stake_account".to_string()), )?; @@ -757,8 +786,8 @@ pub fn process_create_stake_account( } let authorized = Authorized { - staker: staker.unwrap_or(from_pubkey), - withdrawer: withdrawer.unwrap_or(from_pubkey), + staker: staker.unwrap_or(from.pubkey()), + withdrawer: withdrawer.unwrap_or(from.pubkey()), }; let ixs = if let Some(seed) = seed { @@ -783,43 +812,28 @@ pub fn process_create_stake_account( let (recent_blockhash, fee_calculator) = blockhash_query.get_blockhash_fee_calculator(rpc_client)?; - let fee_payer = fee_payer.map(|fp| fp.keypair()).unwrap_or(&config.keypair); - let mut tx_signers = if stake_account_pubkey != from_pubkey { - vec![fee_payer, from, stake_account] // both must sign if `from` and `to` differ - } else { - vec![fee_payer, from] // when stake_account == config.keypair and there's a seed, we only need one signature - }; - let (nonce_authority_pubkey, nonce_authority) = nonce_authority - .map(|na| (na.pubkey(), na.keypair())) - .unwrap_or((config.keypair.pubkey(), &config.keypair)); + let fee_payer = config.signers[fee_payer]; + let nonce_authority = config.signers[nonce_authority]; - let mut tx = if let Some(nonce_account) = &nonce_account { - tx_signers.push(nonce_authority); - Transaction::new_signed_with_nonce( + let message = if let Some(nonce_account) = &nonce_account { + Message::new_with_nonce( ixs, Some(&fee_payer.pubkey()), - &tx_signers, nonce_account, &nonce_authority.pubkey(), - recent_blockhash, ) } else { - Transaction::new_signed_with_payer( - ixs, - Some(&fee_payer.pubkey()), - &tx_signers, - recent_blockhash, - ) + Message::new_with_payer(ixs, Some(&fee_payer.pubkey())) }; - if let Some(signers) = signers { - replace_signatures(&mut tx, &signers)?; - } + let mut tx = Transaction::new_unsigned(message); + tx.try_sign(&config.signers, recent_blockhash)?; + if sign_only { return_signers(&tx) } else { if let Some(nonce_account) = &nonce_account { let nonce_account = rpc_client.get_account(nonce_account)?; - check_nonce_account(&nonce_account, &nonce_authority_pubkey, &recent_blockhash)?; + check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?; } check_account_for_fee( rpc_client, @@ -827,7 +841,7 @@ pub fn process_create_stake_account( &fee_calculator, &tx.message, )?; - let result = rpc_client.send_and_confirm_transaction(&mut tx, &tx_signers); + let result = rpc_client.send_and_confirm_transaction(&mut tx, &config.signers); log_instruction_custom_error::(result) } } @@ -839,19 +853,18 @@ pub fn process_stake_authorize( stake_account_pubkey: &Pubkey, authorized_pubkey: &Pubkey, stake_authorize: StakeAuthorize, - authority: Option<&SigningAuthority>, + authority: SignerIndex, sign_only: bool, - signers: &Option>, blockhash_query: &BlockhashQuery, nonce_account: Option, - nonce_authority: Option<&SigningAuthority>, - fee_payer: Option<&SigningAuthority>, + nonce_authority: SignerIndex, + fee_payer: SignerIndex, ) -> ProcessResult { check_unique_pubkeys( (stake_account_pubkey, "stake_account_pubkey".to_string()), (authorized_pubkey, "new_authorized_pubkey".to_string()), )?; - let authority = authority.map(|a| a.keypair()).unwrap_or(&config.keypair); + let authority = config.signers[authority]; let (recent_blockhash, fee_calculator) = blockhash_query.get_blockhash_fee_calculator(rpc_client)?; let ixs = vec![stake_instruction::authorize( @@ -861,36 +874,28 @@ pub fn process_stake_authorize( stake_authorize, // stake or withdraw )]; - let (nonce_authority, nonce_authority_pubkey) = nonce_authority - .map(|a| (a.keypair(), a.pubkey())) - .unwrap_or((&config.keypair, config.keypair.pubkey())); - let fee_payer = fee_payer.map(|f| f.keypair()).unwrap_or(&config.keypair); - let mut tx = if let Some(nonce_account) = &nonce_account { - Transaction::new_signed_with_nonce( + let nonce_authority = config.signers[nonce_authority]; + let fee_payer = config.signers[fee_payer]; + + let message = if let Some(nonce_account) = &nonce_account { + Message::new_with_nonce( ixs, Some(&fee_payer.pubkey()), - &[fee_payer, nonce_authority, authority], nonce_account, &nonce_authority.pubkey(), - recent_blockhash, ) } else { - Transaction::new_signed_with_payer( - ixs, - Some(&fee_payer.pubkey()), - &[fee_payer, authority], - recent_blockhash, - ) + Message::new_with_payer(ixs, Some(&fee_payer.pubkey())) }; - if let Some(signers) = signers { - replace_signatures(&mut tx, &signers)?; - } + let mut tx = Transaction::new_unsigned(message); + tx.try_sign(&config.signers, recent_blockhash)?; + if sign_only { return_signers(&tx) } else { if let Some(nonce_account) = &nonce_account { let nonce_account = rpc_client.get_account(nonce_account)?; - check_nonce_account(&nonce_account, &nonce_authority_pubkey, &recent_blockhash)?; + check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?; } check_account_for_fee( rpc_client, @@ -898,7 +903,7 @@ pub fn process_stake_authorize( &fee_calculator, &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::(result) } } @@ -908,53 +913,42 @@ pub fn process_deactivate_stake_account( rpc_client: &RpcClient, config: &CliConfig, stake_account_pubkey: &Pubkey, - stake_authority: Option<&SigningAuthority>, + stake_authority: SignerIndex, sign_only: bool, - signers: &Option>, blockhash_query: &BlockhashQuery, nonce_account: Option, - nonce_authority: Option<&SigningAuthority>, - fee_payer: Option<&SigningAuthority>, + nonce_authority: SignerIndex, + fee_payer: SignerIndex, ) -> ProcessResult { let (recent_blockhash, fee_calculator) = blockhash_query.get_blockhash_fee_calculator(rpc_client)?; - let stake_authority = stake_authority - .map(|a| a.keypair()) - .unwrap_or(&config.keypair); + let stake_authority = config.signers[stake_authority]; let ixs = vec![stake_instruction::deactivate_stake( stake_account_pubkey, &stake_authority.pubkey(), )]; - let (nonce_authority, nonce_authority_pubkey) = nonce_authority - .map(|a| (a.keypair(), a.pubkey())) - .unwrap_or((&config.keypair, config.keypair.pubkey())); - let fee_payer = fee_payer.map(|f| f.keypair()).unwrap_or(&config.keypair); - let mut tx = if let Some(nonce_account) = &nonce_account { - Transaction::new_signed_with_nonce( + let nonce_authority = config.signers[nonce_authority]; + let fee_payer = config.signers[fee_payer]; + + let message = if let Some(nonce_account) = &nonce_account { + Message::new_with_nonce( ixs, Some(&fee_payer.pubkey()), - &[fee_payer, nonce_authority, stake_authority], nonce_account, &nonce_authority.pubkey(), - recent_blockhash, ) } else { - Transaction::new_signed_with_payer( - ixs, - Some(&fee_payer.pubkey()), - &[fee_payer, stake_authority], - recent_blockhash, - ) + Message::new_with_payer(ixs, Some(&fee_payer.pubkey())) }; - if let Some(signers) = signers { - replace_signatures(&mut tx, &signers)?; - } + let mut tx = Transaction::new_unsigned(message); + tx.try_sign(&config.signers, recent_blockhash)?; + if sign_only { return_signers(&tx) } else { if let Some(nonce_account) = &nonce_account { let nonce_account = rpc_client.get_account(nonce_account)?; - check_nonce_account(&nonce_account, &nonce_authority_pubkey, &recent_blockhash)?; + check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?; } check_account_for_fee( rpc_client, @@ -962,7 +956,7 @@ pub fn process_deactivate_stake_account( &fee_calculator, &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::(result) } } @@ -974,19 +968,16 @@ pub fn process_withdraw_stake( stake_account_pubkey: &Pubkey, destination_account_pubkey: &Pubkey, lamports: u64, - withdraw_authority: Option<&SigningAuthority>, + withdraw_authority: SignerIndex, sign_only: bool, - signers: Option<&Vec<(Pubkey, Signature)>>, blockhash_query: &BlockhashQuery, nonce_account: Option<&Pubkey>, - nonce_authority: Option<&SigningAuthority>, - fee_payer: Option<&SigningAuthority>, + nonce_authority: SignerIndex, + fee_payer: SignerIndex, ) -> ProcessResult { let (recent_blockhash, fee_calculator) = blockhash_query.get_blockhash_fee_calculator(rpc_client)?; - let withdraw_authority = withdraw_authority - .map(|a| a.keypair()) - .unwrap_or(&config.keypair); + let withdraw_authority = config.signers[withdraw_authority]; let ixs = vec![stake_instruction::withdraw( stake_account_pubkey, @@ -995,36 +986,28 @@ pub fn process_withdraw_stake( lamports, )]; - let fee_payer = fee_payer.map(|fp| fp.keypair()).unwrap_or(&config.keypair); - let (nonce_authority_pubkey, nonce_authority) = nonce_authority - .map(|na| (na.pubkey(), na.keypair())) - .unwrap_or((config.keypair.pubkey(), &config.keypair)); - let mut tx = if let Some(nonce_account) = &nonce_account { - Transaction::new_signed_with_nonce( + let fee_payer = config.signers[fee_payer]; + let nonce_authority = config.signers[nonce_authority]; + + let message = if let Some(nonce_account) = &nonce_account { + Message::new_with_nonce( ixs, Some(&fee_payer.pubkey()), - &[fee_payer, withdraw_authority, nonce_authority], nonce_account, &nonce_authority.pubkey(), - recent_blockhash, ) } else { - Transaction::new_signed_with_payer( - ixs, - Some(&fee_payer.pubkey()), - &[fee_payer, withdraw_authority], - recent_blockhash, - ) + Message::new_with_payer(ixs, Some(&fee_payer.pubkey())) }; - if let Some(signers) = signers { - replace_signatures(&mut tx, &signers)?; - } + let mut tx = Transaction::new_unsigned(message); + tx.try_sign(&config.signers, recent_blockhash)?; + if sign_only { return_signers(&tx) } else { if let Some(nonce_account) = &nonce_account { let nonce_account = rpc_client.get_account(nonce_account)?; - check_nonce_account(&nonce_account, &nonce_authority_pubkey, &recent_blockhash)?; + check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?; } check_account_for_fee( rpc_client, @@ -1032,7 +1015,7 @@ pub fn process_withdraw_stake( &fee_calculator, &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::(result) } } @@ -1042,29 +1025,27 @@ pub fn process_split_stake( rpc_client: &RpcClient, config: &CliConfig, stake_account_pubkey: &Pubkey, - stake_authority: Option<&SigningAuthority>, + stake_authority: SignerIndex, sign_only: bool, - signers: &Option>, blockhash_query: &BlockhashQuery, nonce_account: Option, - nonce_authority: Option<&SigningAuthority>, - split_stake_account: &Keypair, + nonce_authority: SignerIndex, + split_stake_account: SignerIndex, split_stake_account_seed: &Option, lamports: u64, - fee_payer: Option<&SigningAuthority>, + fee_payer: SignerIndex, ) -> ProcessResult { - let (fee_payer_pubkey, fee_payer) = fee_payer - .map(|f| (f.pubkey(), f.keypair())) - .unwrap_or((config.keypair.pubkey(), &config.keypair)); + let split_stake_account = config.signers[split_stake_account]; + let fee_payer = config.signers[fee_payer]; check_unique_pubkeys( - (&fee_payer_pubkey, "fee-payer keypair".to_string()), + (&fee_payer.pubkey(), "fee-payer keypair".to_string()), ( &split_stake_account.pubkey(), "split_stake_account".to_string(), ), )?; check_unique_pubkeys( - (&fee_payer_pubkey, "fee-payer keypair".to_string()), + (&fee_payer.pubkey(), "fee-payer keypair".to_string()), (&stake_account_pubkey, "stake_account".to_string()), )?; check_unique_pubkeys( @@ -1075,9 +1056,7 @@ pub fn process_split_stake( ), )?; - let stake_authority = stake_authority - .map(|a| a.keypair()) - .unwrap_or(&config.keypair); + let stake_authority = config.signers[stake_authority]; let split_stake_account_address = if let Some(seed) = split_stake_account_seed { create_address_with_seed( @@ -1138,41 +1117,27 @@ pub fn process_split_stake( ) }; - let (nonce_authority, nonce_authority_pubkey) = nonce_authority - .map(|a| (a.keypair(), a.pubkey())) - .unwrap_or((&config.keypair, config.keypair.pubkey())); + let nonce_authority = config.signers[nonce_authority]; - let mut tx = if let Some(nonce_account) = &nonce_account { - Transaction::new_signed_with_nonce( + let message = if let Some(nonce_account) = &nonce_account { + Message::new_with_nonce( ixs, Some(&fee_payer.pubkey()), - &[ - fee_payer, - nonce_authority, - stake_authority, - split_stake_account, - ], nonce_account, &nonce_authority.pubkey(), - recent_blockhash, ) } else { - Transaction::new_signed_with_payer( - ixs, - Some(&fee_payer.pubkey()), - &[fee_payer, stake_authority, split_stake_account], - recent_blockhash, - ) + Message::new_with_payer(ixs, Some(&fee_payer.pubkey())) }; - if let Some(signers) = signers { - replace_signatures(&mut tx, &signers)?; - } + let mut tx = Transaction::new_unsigned(message); + tx.try_sign(&config.signers, recent_blockhash)?; + if sign_only { return_signers(&tx) } else { if let Some(nonce_account) = &nonce_account { let nonce_account = rpc_client.get_account(nonce_account)?; - check_nonce_account(&nonce_account, &nonce_authority_pubkey, &recent_blockhash)?; + check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?; } check_account_for_fee( rpc_client, @@ -1180,7 +1145,7 @@ pub fn process_split_stake( &fee_calculator, &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::(result) } } @@ -1191,17 +1156,16 @@ pub fn process_stake_set_lockup( config: &CliConfig, stake_account_pubkey: &Pubkey, lockup: &mut Lockup, - custodian: Option<&SigningAuthority>, + custodian: SignerIndex, sign_only: bool, - signers: &Option>, blockhash_query: &BlockhashQuery, nonce_account: Option, - nonce_authority: Option<&SigningAuthority>, - fee_payer: Option<&SigningAuthority>, + nonce_authority: SignerIndex, + fee_payer: SignerIndex, ) -> ProcessResult { let (recent_blockhash, fee_calculator) = blockhash_query.get_blockhash_fee_calculator(rpc_client)?; - let custodian = custodian.map(|a| a.keypair()).unwrap_or(&config.keypair); + let custodian = config.signers[custodian]; // If new custodian is not explicitly set, default to current custodian if lockup.custodian == Pubkey::default() { lockup.custodian = custodian.pubkey(); @@ -1211,36 +1175,28 @@ pub fn process_stake_set_lockup( lockup, &custodian.pubkey(), )]; - let (nonce_authority, nonce_authority_pubkey) = nonce_authority - .map(|a| (a.keypair(), a.pubkey())) - .unwrap_or((&config.keypair, config.keypair.pubkey())); - let fee_payer = fee_payer.map(|f| f.keypair()).unwrap_or(&config.keypair); - let mut tx = if let Some(nonce_account) = &nonce_account { - Transaction::new_signed_with_nonce( + let nonce_authority = config.signers[nonce_authority]; + let fee_payer = config.signers[fee_payer]; + + let message = if let Some(nonce_account) = &nonce_account { + Message::new_with_nonce( ixs, Some(&fee_payer.pubkey()), - &[fee_payer, nonce_authority, custodian], nonce_account, &nonce_authority.pubkey(), - recent_blockhash, ) } else { - Transaction::new_signed_with_payer( - ixs, - Some(&fee_payer.pubkey()), - &[fee_payer, custodian], - recent_blockhash, - ) + Message::new_with_payer(ixs, Some(&fee_payer.pubkey())) }; - if let Some(signers) = signers { - replace_signatures(&mut tx, &signers)?; - } + let mut tx = Transaction::new_unsigned(message); + tx.try_sign(&config.signers, recent_blockhash)?; + if sign_only { return_signers(&tx) } else { if let Some(nonce_account) = &nonce_account { let nonce_account = rpc_client.get_account(nonce_account)?; - check_nonce_account(&nonce_account, &nonce_authority_pubkey, &recent_blockhash)?; + check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?; } check_account_for_fee( rpc_client, @@ -1248,7 +1204,7 @@ pub fn process_stake_set_lockup( &fee_calculator, &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::(result) } } @@ -1324,7 +1280,7 @@ pub fn process_show_stake_account( if stake_account.owner != solana_stake_program::id() { return Err(CliError::RpcRequestError(format!( "{:?} is not a stake account", - stake_account_pubkey + stake_account_pubkey, )) .into()); } @@ -1380,22 +1336,19 @@ pub fn process_delegate_stake( config: &CliConfig, stake_account_pubkey: &Pubkey, vote_account_pubkey: &Pubkey, - stake_authority: Option<&SigningAuthority>, + stake_authority: SignerIndex, force: bool, sign_only: bool, - signers: &Option>, blockhash_query: &BlockhashQuery, nonce_account: Option, - nonce_authority: Option<&SigningAuthority>, - fee_payer: Option<&SigningAuthority>, + nonce_authority: SignerIndex, + fee_payer: SignerIndex, ) -> ProcessResult { check_unique_pubkeys( - (&config.keypair.pubkey(), "cli keypair".to_string()), + (&config.signers[0].pubkey(), "cli keypair".to_string()), (stake_account_pubkey, "stake_account_pubkey".to_string()), )?; - let stake_authority = stake_authority - .map(|a| a.keypair()) - .unwrap_or(&config.keypair); + let stake_authority = config.signers[stake_authority]; if !sign_only { // Sanity check the vote account to ensure it is attached to a validator that has recently @@ -1450,36 +1403,28 @@ pub fn process_delegate_stake( &stake_authority.pubkey(), vote_account_pubkey, )]; - let (nonce_authority, nonce_authority_pubkey) = nonce_authority - .map(|a| (a.keypair(), a.pubkey())) - .unwrap_or((&config.keypair, config.keypair.pubkey())); - let fee_payer = fee_payer.map(|f| f.keypair()).unwrap_or(&config.keypair); - let mut tx = if let Some(nonce_account) = &nonce_account { - Transaction::new_signed_with_nonce( + let nonce_authority = config.signers[nonce_authority]; + let fee_payer = config.signers[fee_payer]; + + let message = if let Some(nonce_account) = &nonce_account { + Message::new_with_nonce( ixs, Some(&fee_payer.pubkey()), - &[fee_payer, nonce_authority, stake_authority], nonce_account, &nonce_authority.pubkey(), - recent_blockhash, ) } else { - Transaction::new_signed_with_payer( - ixs, - Some(&fee_payer.pubkey()), - &[fee_payer, stake_authority], - recent_blockhash, - ) + Message::new_with_payer(ixs, Some(&fee_payer.pubkey())) }; - if let Some(signers) = signers { - replace_signatures(&mut tx, &signers)?; - } + let mut tx = Transaction::new_unsigned(message); + tx.try_sign(&config.signers, recent_blockhash)?; + if sign_only { return_signers(&tx) } else { if let Some(nonce_account) = &nonce_account { let nonce_account = rpc_client.get_account(nonce_account)?; - check_nonce_account(&nonce_account, &nonce_authority_pubkey, &recent_blockhash)?; + check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?; } check_account_for_fee( rpc_client, @@ -1487,7 +1432,7 @@ pub fn process_delegate_stake( &fee_calculator, &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::(result) } } @@ -1499,7 +1444,9 @@ mod tests { use solana_sdk::{ fee_calculator::FeeCalculator, hash::Hash, - signature::{keypair_from_seed, read_keypair_file, write_keypair, Signer}, + signature::{ + keypair_from_seed, read_keypair_file, write_keypair, Keypair, Presigner, Signer, + }, }; use tempfile::NamedTempFile; @@ -1514,6 +1461,10 @@ mod tests { authority_keypair_file: &str, stake_authorize: StakeAuthorize, ) { + 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 stake_account_string = stake_account_pubkey.to_string(); let (subcommand, authority_flag) = match stake_authorize { @@ -1529,21 +1480,20 @@ mod tests { &stake_account_string, ]); assert_eq!( - parse_command(&test_authorize).unwrap(), + parse_command(&test_authorize, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, new_authorized_pubkey: stake_account_pubkey, stake_authorize, - authority: None, + authority: 0, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::default(), nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }, - require_keypair: true + signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()], } ); // Test Staker Subcommand w/ authority @@ -1556,21 +1506,23 @@ mod tests { &authority_keypair_file, ]); assert_eq!( - parse_command(&test_authorize).unwrap(), + parse_command(&test_authorize, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, new_authorized_pubkey: stake_account_pubkey, stake_authorize, - authority: Some(read_keypair_file(&authority_keypair_file).unwrap().into()), + authority: 1, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::default(), nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }, - require_keypair: true + signers: vec![ + read_keypair_file(&default_keypair_file).unwrap().into(), + read_keypair_file(&authority_keypair_file).unwrap().into() + ], } ); // Test Authorize Subcommand w/ sign-only @@ -1586,25 +1538,25 @@ mod tests { "--sign-only", ]); assert_eq!( - parse_command(&test_authorize).unwrap(), + parse_command(&test_authorize, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, new_authorized_pubkey: stake_account_pubkey, stake_authorize, - authority: None, + authority: 0, sign_only: true, - signers: None, blockhash_query: BlockhashQuery::None(blockhash, FeeCalculator::default()), nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }, - require_keypair: true + signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()], } ); - // Test Authorize Subcommand w/ signer + // Test Authorize Subcommand w/ offline feepayer let keypair = Keypair::new(); + let pubkey = keypair.pubkey(); let sig = keypair.sign_message(&[0u8]); let signer = format!("{}={}", keypair.pubkey(), sig); let test_authorize = test_commands.clone().get_matches_from(vec![ @@ -1616,29 +1568,35 @@ mod tests { &blockhash_string, "--signer", &signer, + "--fee-payer", + &pubkey.to_string(), ]); assert_eq!( - parse_command(&test_authorize).unwrap(), + parse_command(&test_authorize, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, new_authorized_pubkey: stake_account_pubkey, stake_authorize, - authority: None, + authority: 0, sign_only: false, - signers: Some(vec![(keypair.pubkey(), sig)]), blockhash_query: BlockhashQuery::FeeCalculator(blockhash), nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 1, }, - require_keypair: true + signers: vec![ + read_keypair_file(&default_keypair_file).unwrap().into(), + Presigner::new(&pubkey, &sig).into() + ], } ); - // Test Authorize Subcommand w/ signers + // Test Authorize Subcommand w/ offline fee payer and nonce authority let keypair2 = Keypair::new(); + let pubkey2 = keypair2.pubkey(); let sig2 = keypair.sign_message(&[0u8]); let signer2 = format!("{}={}", keypair2.pubkey(), sig2); + let nonce_account = Pubkey::new(&[1u8; 32]); let test_authorize = test_commands.clone().get_matches_from(vec![ "test", &subcommand, @@ -1650,23 +1608,32 @@ mod tests { &signer, "--signer", &signer2, + "--fee-payer", + &pubkey.to_string(), + "--nonce", + &nonce_account.to_string(), + "--nonce-authority", + &pubkey2.to_string(), ]); assert_eq!( - parse_command(&test_authorize).unwrap(), + parse_command(&test_authorize, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, new_authorized_pubkey: stake_account_pubkey, stake_authorize, - authority: None, + authority: 0, sign_only: false, - signers: Some(vec![(keypair.pubkey(), sig), (keypair2.pubkey(), sig2),]), blockhash_query: BlockhashQuery::FeeCalculator(blockhash), - nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_account: Some(nonce_account), + nonce_authority: 2, + fee_payer: 1, }, - require_keypair: true + signers: vec![ + read_keypair_file(&default_keypair_file).unwrap().into(), + Presigner::new(&pubkey, &sig).into(), + Presigner::new(&pubkey2, &sig2).into(), + ], } ); // Test Authorize Subcommand w/ blockhash @@ -1679,21 +1646,20 @@ mod tests { &blockhash_string, ]); assert_eq!( - parse_command(&test_authorize).unwrap(), + parse_command(&test_authorize, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, new_authorized_pubkey: stake_account_pubkey, stake_authorize, - authority: None, + authority: 0, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::FeeCalculator(blockhash), nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }, - require_keypair: true + signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()], } ); // Test Authorize Subcommand w/ nonce @@ -1715,21 +1681,23 @@ mod tests { &nonce_keypair_file, ]); assert_eq!( - parse_command(&test_authorize).unwrap(), + parse_command(&test_authorize, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, new_authorized_pubkey: stake_account_pubkey, stake_authorize, - authority: None, + authority: 0, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::FeeCalculator(blockhash), nonce_account: Some(nonce_account_pubkey), - nonce_authority: Some(nonce_authority_keypair.into()), - fee_payer: None, + nonce_authority: 1, + fee_payer: 0, }, - require_keypair: true + signers: vec![ + read_keypair_file(&default_keypair_file).unwrap().into(), + nonce_authority_keypair.into() + ], } ); // Test Authorize Subcommand w/ fee-payer @@ -1747,21 +1715,23 @@ mod tests { &fee_payer_keypair_file, ]); assert_eq!( - parse_command(&test_authorize).unwrap(), + parse_command(&test_authorize, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, new_authorized_pubkey: stake_account_pubkey, stake_authorize, - authority: None, + authority: 0, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::All, nonce_account: None, - nonce_authority: None, - fee_payer: Some(read_keypair_file(&fee_payer_keypair_file).unwrap().into()), + nonce_authority: 0, + fee_payer: 1, }, - require_keypair: true + signers: vec![ + read_keypair_file(&default_keypair_file).unwrap().into(), + read_keypair_file(&fee_payer_keypair_file).unwrap().into(), + ], } ); // Test Authorize Subcommand w/ absentee fee-payer @@ -1780,21 +1750,23 @@ mod tests { &signer, ]); assert_eq!( - parse_command(&test_authorize).unwrap(), + parse_command(&test_authorize, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, new_authorized_pubkey: stake_account_pubkey, stake_authorize, - authority: None, + authority: 0, sign_only: false, - signers: Some(vec![(fee_payer_pubkey, sig)]), blockhash_query: BlockhashQuery::FeeCalculator(blockhash), nonce_account: None, - nonce_authority: None, - fee_payer: Some(fee_payer_pubkey.into()), + nonce_authority: 0, + fee_payer: 1, }, - require_keypair: true + signers: vec![ + read_keypair_file(&default_keypair_file).unwrap().into(), + Presigner::new(&fee_payer_pubkey, &sig).into() + ], } ); } @@ -1802,6 +1774,9 @@ mod tests { #[test] fn test_parse_command() { 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 stake_account_keypair = Keypair::new(); write_keypair(&stake_account_keypair, tmp_file.as_file_mut()).unwrap(); @@ -1843,10 +1818,10 @@ mod tests { "43", ]); assert_eq!( - parse_command(&test_create_stake_account).unwrap(), + parse_command(&test_create_stake_account, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::CreateStakeAccount { - stake_account: stake_account_keypair.into(), + stake_account: 1, seed: None, staker: Some(authorized), withdrawer: Some(authorized), @@ -1857,14 +1832,16 @@ mod tests { }, lamports: 50_000_000_000, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::All, nonce_account: None, - nonce_authority: None, - fee_payer: None, - from: None, + nonce_authority: 0, + fee_payer: 0, + from: 0, }, - require_keypair: true + signers: vec![ + read_keypair_file(&default_keypair_file).unwrap().into(), + stake_account_keypair.into() + ], } ); @@ -1882,24 +1859,26 @@ mod tests { ]); assert_eq!( - parse_command(&test_create_stake_account2).unwrap(), + parse_command(&test_create_stake_account2, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::CreateStakeAccount { - stake_account: read_keypair_file(&keypair_file).unwrap().into(), + stake_account: 1, seed: None, staker: None, withdrawer: None, lockup: Lockup::default(), lamports: 50_000_000_000, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::All, nonce_account: None, - nonce_authority: None, - fee_payer: None, - from: None, + nonce_authority: 0, + fee_payer: 0, + from: 0, }, - require_keypair: true + signers: vec![ + read_keypair_file(&default_keypair_file).unwrap().into(), + read_keypair_file(&keypair_file).unwrap().into() + ], } ); @@ -1933,24 +1912,26 @@ mod tests { ]); assert_eq!( - parse_command(&test_create_stake_account2).unwrap(), + parse_command(&test_create_stake_account2, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::CreateStakeAccount { - stake_account: read_keypair_file(&keypair_file).unwrap().into(), + stake_account: 1, seed: None, staker: None, withdrawer: None, lockup: Lockup::default(), lamports: 50_000_000_000, sign_only: false, - signers: Some(vec![(offline_pubkey, offline_sig)]), blockhash_query: BlockhashQuery::FeeCalculator(nonce_hash), nonce_account: Some(nonce_account), - nonce_authority: Some(offline_pubkey.into()), - fee_payer: Some(offline_pubkey.into()), - from: Some(offline_pubkey.into()), + nonce_authority: 0, + fee_payer: 0, + from: 0, }, - require_keypair: false, + signers: vec![ + Presigner::new(&offline_pubkey, &offline_sig).into(), + read_keypair_file(&keypair_file).unwrap().into() + ], } ); @@ -1964,21 +1945,20 @@ mod tests { &vote_account_string, ]); assert_eq!( - parse_command(&test_delegate_stake).unwrap(), + parse_command(&test_delegate_stake, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::DelegateStake { stake_account_pubkey, vote_account_pubkey, - stake_authority: None, + stake_authority: 0, force: false, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::default(), nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }, - require_keypair: true + signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()], } ); @@ -1994,25 +1974,25 @@ mod tests { &stake_authority_keypair_file, ]); assert_eq!( - parse_command(&test_delegate_stake).unwrap(), + parse_command(&test_delegate_stake, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::DelegateStake { stake_account_pubkey, vote_account_pubkey, - stake_authority: Some( - read_keypair_file(&stake_authority_keypair_file) - .unwrap() - .into() - ), + stake_authority: 1, force: false, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::default(), nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }, - require_keypair: true + signers: vec![ + read_keypair_file(&default_keypair_file).unwrap().into(), + read_keypair_file(&stake_authority_keypair_file) + .unwrap() + .into() + ], } ); @@ -2025,21 +2005,20 @@ mod tests { &vote_account_string, ]); assert_eq!( - parse_command(&test_delegate_stake).unwrap(), + parse_command(&test_delegate_stake, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::DelegateStake { stake_account_pubkey, vote_account_pubkey, - stake_authority: None, + stake_authority: 0, force: true, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::default(), nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }, - require_keypair: true + signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()], } ); @@ -2055,21 +2034,20 @@ mod tests { &blockhash_string, ]); assert_eq!( - parse_command(&test_delegate_stake).unwrap(), + parse_command(&test_delegate_stake, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::DelegateStake { stake_account_pubkey, vote_account_pubkey, - stake_authority: None, + stake_authority: 0, force: false, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::FeeCalculator(blockhash), nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }, - require_keypair: true + signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()], } ); @@ -2083,25 +2061,24 @@ mod tests { "--sign-only", ]); assert_eq!( - parse_command(&test_delegate_stake).unwrap(), + parse_command(&test_delegate_stake, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::DelegateStake { stake_account_pubkey, vote_account_pubkey, - stake_authority: None, + stake_authority: 0, force: false, sign_only: true, - signers: None, blockhash_query: BlockhashQuery::None(blockhash, FeeCalculator::default()), nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }, - require_keypair: true + signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()], } ); - // Test Delegate Subcommand w/ signer + // Test Delegate Subcommand w/ absent fee payer let key1 = Pubkey::new_rand(); let sig1 = Keypair::new().sign_message(&[0u8]); let signer1 = format!("{}={}", key1, sig1); @@ -2114,27 +2091,31 @@ mod tests { &blockhash_string, "--signer", &signer1, + "--fee-payer", + &key1.to_string(), ]); assert_eq!( - parse_command(&test_delegate_stake).unwrap(), + parse_command(&test_delegate_stake, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::DelegateStake { stake_account_pubkey, vote_account_pubkey, - stake_authority: None, + stake_authority: 0, force: false, sign_only: false, - signers: Some(vec![(key1, sig1)]), blockhash_query: BlockhashQuery::FeeCalculator(blockhash), nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 1, }, - require_keypair: false + signers: vec![ + read_keypair_file(&default_keypair_file).unwrap().into(), + Presigner::new(&key1, &sig1).into() + ], } ); - // Test Delegate Subcommand w/ signers + // Test Delegate Subcommand w/ absent fee payer and absent nonce authority let key2 = Pubkey::new_rand(); let sig2 = Keypair::new().sign_message(&[0u8]); let signer2 = format!("{}={}", key2, sig2); @@ -2149,32 +2130,39 @@ mod tests { &signer1, "--signer", &signer2, + "--fee-payer", + &key1.to_string(), + "--nonce", + &nonce_account.to_string(), + "--nonce-authority", + &key2.to_string(), ]); assert_eq!( - parse_command(&test_delegate_stake).unwrap(), + parse_command(&test_delegate_stake, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::DelegateStake { stake_account_pubkey, vote_account_pubkey, - stake_authority: None, + stake_authority: 0, force: false, sign_only: false, - signers: Some(vec![(key1, sig1), (key2, sig2)]), blockhash_query: BlockhashQuery::FeeCalculator(blockhash), - nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_account: Some(nonce_account), + nonce_authority: 2, + fee_payer: 1, }, - require_keypair: false + signers: vec![ + read_keypair_file(&default_keypair_file).unwrap().into(), + Presigner::new(&key1, &sig1).into(), + Presigner::new(&key2, &sig2).into(), + ], } ); - // Test Delegate Subcommand w/ fee-payer + // Test Delegate Subcommand w/ present fee-payer let (fee_payer_keypair_file, mut fee_payer_tmp_file) = make_tmp_file(); let fee_payer_keypair = Keypair::new(); write_keypair(&fee_payer_keypair, fee_payer_tmp_file.as_file_mut()).unwrap(); - let fee_payer_pubkey = fee_payer_keypair.pubkey(); - let fee_payer_string = fee_payer_pubkey.to_string(); let test_delegate_stake = test_commands.clone().get_matches_from(vec![ "test", "delegate-stake", @@ -2184,55 +2172,23 @@ mod tests { &fee_payer_keypair_file, ]); assert_eq!( - parse_command(&test_delegate_stake).unwrap(), + parse_command(&test_delegate_stake, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::DelegateStake { stake_account_pubkey, vote_account_pubkey, - stake_authority: None, + stake_authority: 0, force: false, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::All, nonce_account: None, - nonce_authority: None, - fee_payer: Some(read_keypair_file(&fee_payer_keypair_file).unwrap().into()), + nonce_authority: 0, + fee_payer: 1, }, - require_keypair: true - } - ); - - // Test Delegate Subcommand w/ absentee fee-payer - let sig = fee_payer_keypair.sign_message(&[0u8]); - let signer = format!("{}={}", fee_payer_string, sig); - let test_delegate_stake = test_commands.clone().get_matches_from(vec![ - "test", - "delegate-stake", - &stake_account_string, - &vote_account_string, - "--fee-payer", - &fee_payer_string, - "--blockhash", - &blockhash_string, - "--signer", - &signer, - ]); - assert_eq!( - parse_command(&test_delegate_stake).unwrap(), - CliCommandInfo { - command: CliCommand::DelegateStake { - stake_account_pubkey, - vote_account_pubkey, - stake_authority: None, - force: false, - sign_only: false, - signers: Some(vec![(fee_payer_pubkey, sig)]), - blockhash_query: BlockhashQuery::FeeCalculator(blockhash), - nonce_account: None, - nonce_authority: None, - fee_payer: Some(fee_payer_pubkey.into()), - }, - require_keypair: false + signers: vec![ + read_keypair_file(&default_keypair_file).unwrap().into(), + read_keypair_file(&fee_payer_keypair_file).unwrap().into() + ], } ); @@ -2246,21 +2202,20 @@ mod tests { ]); assert_eq!( - parse_command(&test_withdraw_stake).unwrap(), + parse_command(&test_withdraw_stake, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::WithdrawStake { stake_account_pubkey, destination_account_pubkey: stake_account_pubkey, lamports: 42_000_000_000, - withdraw_authority: None, + withdraw_authority: 0, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::All, nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }, - require_keypair: true + signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()], } ); @@ -2276,25 +2231,25 @@ mod tests { ]); assert_eq!( - parse_command(&test_withdraw_stake).unwrap(), + parse_command(&test_withdraw_stake, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::WithdrawStake { stake_account_pubkey, destination_account_pubkey: stake_account_pubkey, lamports: 42_000_000_000, - withdraw_authority: Some( - read_keypair_file(&stake_authority_keypair_file) - .unwrap() - .into() - ), + withdraw_authority: 1, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::All, nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }, - require_keypair: true + signers: vec![ + read_keypair_file(&default_keypair_file).unwrap().into(), + read_keypair_file(&stake_authority_keypair_file) + .unwrap() + .into() + ], } ); @@ -2320,25 +2275,25 @@ mod tests { ]); assert_eq!( - parse_command(&test_withdraw_stake).unwrap(), + parse_command(&test_withdraw_stake, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::WithdrawStake { stake_account_pubkey, destination_account_pubkey: stake_account_pubkey, lamports: 42_000_000_000, - withdraw_authority: Some( - read_keypair_file(&stake_authority_keypair_file) - .unwrap() - .into() - ), + withdraw_authority: 0, sign_only: false, - signers: Some(vec![(offline_pubkey, offline_sig)]), blockhash_query: BlockhashQuery::FeeCalculator(nonce_hash), nonce_account: Some(nonce_account), - nonce_authority: Some(offline_pubkey.into()), - fee_payer: Some(offline_pubkey.into()), + nonce_authority: 1, + fee_payer: 1, }, - require_keypair: false, + signers: vec![ + read_keypair_file(&stake_authority_keypair_file) + .unwrap() + .into(), + Presigner::new(&offline_pubkey, &offline_sig).into() + ], } ); @@ -2349,19 +2304,18 @@ mod tests { &stake_account_string, ]); assert_eq!( - parse_command(&test_deactivate_stake).unwrap(), + parse_command(&test_deactivate_stake, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::DeactivateStake { stake_account_pubkey, - stake_authority: None, + stake_authority: 0, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::default(), nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }, - require_keypair: true + signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()], } ); @@ -2374,23 +2328,23 @@ mod tests { &stake_authority_keypair_file, ]); assert_eq!( - parse_command(&test_deactivate_stake).unwrap(), + parse_command(&test_deactivate_stake, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::DeactivateStake { stake_account_pubkey, - stake_authority: Some( - read_keypair_file(&stake_authority_keypair_file) - .unwrap() - .into() - ), + stake_authority: 1, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::default(), nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }, - require_keypair: true + signers: vec![ + read_keypair_file(&default_keypair_file).unwrap().into(), + read_keypair_file(&stake_authority_keypair_file) + .unwrap() + .into() + ], } ); @@ -2405,19 +2359,18 @@ mod tests { &blockhash_string, ]); assert_eq!( - parse_command(&test_deactivate_stake).unwrap(), + parse_command(&test_deactivate_stake, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::DeactivateStake { stake_account_pubkey, - stake_authority: None, + stake_authority: 0, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::FeeCalculator(blockhash), nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }, - require_keypair: true + signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()], } ); @@ -2430,23 +2383,22 @@ mod tests { "--sign-only", ]); assert_eq!( - parse_command(&test_deactivate_stake).unwrap(), + parse_command(&test_deactivate_stake, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::DeactivateStake { stake_account_pubkey, - stake_authority: None, + stake_authority: 0, sign_only: true, - signers: None, blockhash_query: BlockhashQuery::None(blockhash, FeeCalculator::default()), nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }, - require_keypair: true + signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()], } ); - // Test Deactivate Subcommand w/ signers + // Test Deactivate Subcommand w/ absent fee payer let key1 = Pubkey::new_rand(); let sig1 = Keypair::new().sign_message(&[0u8]); let signer1 = format!("{}={}", key1, sig1); @@ -2458,25 +2410,29 @@ mod tests { &blockhash_string, "--signer", &signer1, + "--fee-payer", + &key1.to_string(), ]); assert_eq!( - parse_command(&test_deactivate_stake).unwrap(), + parse_command(&test_deactivate_stake, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::DeactivateStake { stake_account_pubkey, - stake_authority: None, + stake_authority: 0, sign_only: false, - signers: Some(vec![(key1, sig1)]), blockhash_query: BlockhashQuery::FeeCalculator(blockhash), nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 1, }, - require_keypair: false + signers: vec![ + read_keypair_file(&default_keypair_file).unwrap().into(), + Presigner::new(&key1, &sig1).into() + ], } ); - // Test Deactivate Subcommand w/ signers + // Test Deactivate Subcommand w/ absent fee payer and nonce authority let key2 = Pubkey::new_rand(); let sig2 = Keypair::new().sign_message(&[0u8]); let signer2 = format!("{}={}", key2, sig2); @@ -2490,21 +2446,30 @@ mod tests { &signer1, "--signer", &signer2, + "--fee-payer", + &key1.to_string(), + "--nonce", + &nonce_account.to_string(), + "--nonce-authority", + &key2.to_string(), ]); assert_eq!( - parse_command(&test_deactivate_stake).unwrap(), + parse_command(&test_deactivate_stake, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::DeactivateStake { stake_account_pubkey, - stake_authority: None, + stake_authority: 0, sign_only: false, - signers: Some(vec![(key1, sig1), (key2, sig2)]), blockhash_query: BlockhashQuery::FeeCalculator(blockhash), - nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_account: Some(nonce_account), + nonce_authority: 2, + fee_payer: 1, }, - require_keypair: false + signers: vec![ + read_keypair_file(&default_keypair_file).unwrap().into(), + Presigner::new(&key1, &sig1).into(), + Presigner::new(&key2, &sig2).into(), + ], } ); @@ -2517,50 +2482,21 @@ mod tests { &fee_payer_keypair_file, ]); assert_eq!( - parse_command(&test_deactivate_stake).unwrap(), + parse_command(&test_deactivate_stake, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::DeactivateStake { stake_account_pubkey, - stake_authority: None, + stake_authority: 0, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::All, nonce_account: None, - nonce_authority: None, - fee_payer: Some(read_keypair_file(&fee_payer_keypair_file).unwrap().into()), + nonce_authority: 0, + fee_payer: 1, }, - require_keypair: true - } - ); - - // Test Deactivate Subcommand w/ absentee fee-payer - let sig = fee_payer_keypair.sign_message(&[0u8]); - let signer = format!("{}={}", fee_payer_string, sig); - let test_deactivate_stake = test_commands.clone().get_matches_from(vec![ - "test", - "deactivate-stake", - &stake_account_string, - "--fee-payer", - &fee_payer_string, - "--blockhash", - &blockhash_string, - "--signer", - &signer, - ]); - assert_eq!( - parse_command(&test_deactivate_stake).unwrap(), - CliCommandInfo { - command: CliCommand::DeactivateStake { - stake_account_pubkey, - stake_authority: None, - sign_only: false, - signers: Some(vec![(fee_payer_pubkey, sig)]), - blockhash_query: BlockhashQuery::FeeCalculator(blockhash), - nonce_account: None, - nonce_authority: None, - fee_payer: Some(fee_payer_pubkey.into()), - }, - require_keypair: false + signers: vec![ + read_keypair_file(&default_keypair_file).unwrap().into(), + read_keypair_file(&fee_payer_keypair_file).unwrap().into() + ], } ); @@ -2580,24 +2516,26 @@ mod tests { "50", ]); assert_eq!( - parse_command(&test_split_stake_account).unwrap(), + parse_command(&test_split_stake_account, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::SplitStake { stake_account_pubkey: stake_account_keypair.pubkey(), - stake_authority: None, + stake_authority: 0, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::default(), nonce_account: None, - nonce_authority: None, - split_stake_account: read_keypair_file(&split_stake_account_keypair_file) - .unwrap() - .into(), + nonce_authority: 0, + split_stake_account: 1, seed: None, lamports: 50_000_000_000, - fee_payer: None, + fee_payer: 0, }, - require_keypair: true + signers: vec![ + read_keypair_file(&default_keypair_file).unwrap().into(), + read_keypair_file(&split_stake_account_keypair_file) + .unwrap() + .into() + ], } ); @@ -2639,27 +2577,27 @@ mod tests { &stake_signer, ]); assert_eq!( - parse_command(&test_split_stake_account).unwrap(), + parse_command(&test_split_stake_account, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::SplitStake { stake_account_pubkey: stake_account_keypair.pubkey(), - stake_authority: Some(stake_auth_pubkey.into()), + stake_authority: 0, sign_only: false, - signers: Some(vec![ - (nonce_auth_pubkey, nonce_sig), - (stake_auth_pubkey, stake_sig) - ]), blockhash_query: BlockhashQuery::FeeCalculator(nonce_hash), nonce_account: Some(nonce_account.into()), - nonce_authority: Some(nonce_auth_pubkey.into()), - split_stake_account: read_keypair_file(&split_stake_account_keypair_file) - .unwrap() - .into(), + nonce_authority: 1, + split_stake_account: 2, seed: None, lamports: 50_000_000_000, - fee_payer: Some(nonce_auth_pubkey.into()), + fee_payer: 1, }, - require_keypair: false, + signers: vec![ + Presigner::new(&stake_auth_pubkey, &stake_sig).into(), + Presigner::new(&nonce_auth_pubkey, &nonce_sig).into(), + read_keypair_file(&split_stake_account_keypair_file) + .unwrap() + .into(), + ], } ); } diff --git a/cli/src/storage.rs b/cli/src/storage.rs index dba719dee1..faa01081dd 100644 --- a/cli/src/storage.rs +++ b/cli/src/storage.rs @@ -1,16 +1,17 @@ use crate::cli::{ 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 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_sdk::signature::Keypair; +use solana_remote_wallet::remote_wallet::RemoteWalletManager; use solana_sdk::{ - account_utils::StateMut, message::Message, pubkey::Pubkey, signature::Signer, - system_instruction::SystemError, transaction::Transaction, + account_utils::StateMut, message::Message, pubkey::Pubkey, system_instruction::SystemError, + transaction::Transaction, }; use solana_storage_program::storage_instruction::{self, StorageAccountType}; +use std::sync::Arc; pub trait StorageSubCommands { fn storage_subcommands(self) -> Self; @@ -99,35 +100,49 @@ impl StorageSubCommands for App<'_, '_> { pub fn parse_storage_create_archiver_account( matches: &ArgMatches<'_>, + default_signer_path: &str, + wallet_manager: Option<&Arc>, ) -> Result { let account_owner = pubkey_of(matches, "storage_account_owner").unwrap(); let storage_account = keypair_of(matches, "storage_account").unwrap(); Ok(CliCommandInfo { command: CliCommand::CreateStorageAccount { account_owner, - storage_account: storage_account.into(), + storage_account: 1, 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( matches: &ArgMatches<'_>, + default_signer_path: &str, + wallet_manager: Option<&Arc>, ) -> Result { let account_owner = pubkey_of(matches, "storage_account_owner").unwrap(); let storage_account = keypair_of(matches, "storage_account").unwrap(); Ok(CliCommandInfo { command: CliCommand::CreateStorageAccount { account_owner, - storage_account: storage_account.into(), + storage_account: 1, 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 { +pub fn parse_storage_claim_reward( + matches: &ArgMatches<'_>, + default_signer_path: &str, + wallet_manager: Option<&Arc>, +) -> Result { let node_account_pubkey = pubkey_of(matches, "node_account_pubkey").unwrap(); let storage_account_pubkey = pubkey_of(matches, "storage_account_pubkey").unwrap(); Ok(CliCommandInfo { @@ -135,7 +150,12 @@ pub fn parse_storage_claim_reward(matches: &ArgMatches<'_>) -> Result ProcessResult { + let storage_account = config.signers[storage_account]; let storage_account_pubkey = storage_account.pubkey(); check_unique_pubkeys( - (&config.keypair.pubkey(), "cli keypair".to_string()), + (&config.signers[0].pubkey(), "cli keypair".to_string()), ( &storage_account_pubkey, "storage_account_pubkey".to_string(), @@ -183,7 +204,7 @@ pub fn process_create_storage_account( .max(1); let ixs = storage_instruction::create_storage_account( - &config.keypair.pubkey(), + &config.signers[0].pubkey(), &account_owner, &storage_account_pubkey, required_balance, @@ -191,19 +212,16 @@ pub fn process_create_storage_account( ); let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; - let mut tx = Transaction::new_signed_instructions( - &[&config.keypair, &storage_account], - ixs, - recent_blockhash, - ); + let message = Message::new(ixs); + let mut tx = Transaction::new_unsigned(message); + tx.try_sign(&config.signers, recent_blockhash)?; check_account_for_fee( rpc_client, - &config.keypair.pubkey(), + &config.signers[0].pubkey(), &fee_calculator, &tx.message, )?; - let result = - rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair, &storage_account]); + let result = rpc_client.send_and_confirm_transaction(&mut tx, &config.signers); log_instruction_custom_error::(result) } @@ -217,13 +235,13 @@ pub fn process_claim_storage_reward( let instruction = 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 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( rpc_client, - &config.keypair.pubkey(), + &config.signers[0].pubkey(), &fee_calculator, &tx.message, )?; @@ -259,7 +277,7 @@ pub fn process_show_storage_account( mod tests { use super::*; 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; fn make_tmp_file() -> (String, NamedTempFile) { @@ -273,6 +291,10 @@ mod tests { let pubkey = Pubkey::new_rand(); 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 storage_account_keypair = Keypair::new(); write_keypair(&storage_account_keypair, tmp_file.as_file_mut()).unwrap(); @@ -284,14 +306,22 @@ mod tests { &keypair_file, ]); assert_eq!( - parse_command(&test_create_archiver_storage_account).unwrap(), + parse_command( + &test_create_archiver_storage_account, + &default_keypair_file, + None + ) + .unwrap(), CliCommandInfo { command: CliCommand::CreateStorageAccount { account_owner: pubkey, - storage_account: storage_account_keypair.into(), + storage_account: 1, 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, ]); assert_eq!( - parse_command(&test_create_validator_storage_account).unwrap(), + parse_command( + &test_create_validator_storage_account, + &default_keypair_file, + None + ) + .unwrap(), CliCommandInfo { command: CliCommand::CreateStorageAccount { account_owner: pubkey, - storage_account: storage_account_keypair.into(), + storage_account: 1, 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, ]); assert_eq!( - parse_command(&test_claim_storage_reward).unwrap(), + parse_command(&test_claim_storage_reward, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::ClaimStorageReward { node_account_pubkey: pubkey, storage_account_pubkey, }, - require_keypair: true + signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()], } ); } diff --git a/cli/src/validator_info.rs b/cli/src/validator_info.rs index 54777d6c18..c902758eaa 100644 --- a/cli/src/validator_info.rs +++ b/cli/src/validator_info.rs @@ -11,9 +11,11 @@ use serde_json::{Map, Value}; use solana_clap_utils::{ input_parsers::pubkey_of, input_validators::{is_pubkey, is_url}, + keypair::signer_from_path, }; use solana_client::rpc_client::RpcClient; use solana_config_program::{config_instruction, get_config_data, ConfigKeys, ConfigState}; +use solana_remote_wallet::remote_wallet::RemoteWalletManager; use solana_sdk::{ account::Account, commitment_config::CommitmentConfig, @@ -22,7 +24,7 @@ use solana_sdk::{ signature::{Keypair, Signer}, transaction::Transaction, }; -use std::error; +use std::{error, sync::Arc}; use titlecase::titlecase; 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 { +pub fn parse_validator_info_command( + matches: &ArgMatches<'_>, + default_signer_path: &str, + wallet_manager: Option<&Arc>, +) -> Result { let info_pubkey = pubkey_of(matches, "info_pubkey"); // Prepare validator info - let validator_info = parse_args(&matches); + let validator_info = parse_args(matches); Ok(CliCommandInfo { command: CliCommand::SetValidatorInfo { validator_info, force_keybase: matches.is_present("force"), 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"); Ok(CliCommandInfo { command: CliCommand::GetValidatorInfo(info_pubkey), - require_keypair: false, + signers: vec![], }) } @@ -257,7 +268,7 @@ pub fn process_set_validator_info( ) -> ProcessResult { // Validate keybase username 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 force_keybase { println!("--force supplied, ignoring: {:?}", result); @@ -282,7 +293,7 @@ pub fn process_set_validator_info( }) .find(|(pubkey, account)| { 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 @@ -300,8 +311,8 @@ pub fn process_set_validator_info( .poll_get_balance_with_commitment(&info_pubkey, CommitmentConfig::default()) .unwrap_or(0); - let keys = vec![(id(), false), (config.keypair.pubkey(), true)]; - let (message, signers): (Message, Vec<&Keypair>) = if balance == 0 { + let keys = vec![(id(), false), (config.signers[0].pubkey(), true)]; + let (message, signers): (Message, Vec<&dyn Signer>) = if balance == 0 { if info_pubkey != info_keypair.pubkey() { println!( "Account {:?} does not exist. Generating new keypair...", @@ -311,12 +322,12 @@ pub fn process_set_validator_info( } println!( "Publishing info for Validator {:?}", - config.keypair.pubkey() + config.signers[0].pubkey() ); let lamports = rpc_client .get_minimum_balance_for_rent_exemption(ValidatorInfo::max_space() as usize)?; let mut instructions = config_instruction::create_account::( - &config.keypair.pubkey(), + &config.signers[0].pubkey(), &info_keypair.pubkey(), lamports, keys.clone(), @@ -327,13 +338,13 @@ pub fn process_set_validator_info( keys, &validator_info, )]); - let signers = vec![&config.keypair, &info_keypair]; + let signers = vec![config.signers[0], &info_keypair]; let message = Message::new(instructions); (message, signers) } else { println!( "Updating Validator {:?} info at: {:?}", - config.keypair.pubkey(), + config.signers[0].pubkey(), info_pubkey ); let instructions = vec![config_instruction::store( @@ -342,17 +353,18 @@ pub fn process_set_validator_info( keys, &validator_info, )]; - let message = Message::new_with_payer(instructions, Some(&config.keypair.pubkey())); - let signers = vec![&config.keypair]; + let message = Message::new_with_payer(instructions, Some(&config.signers[0].pubkey())); + let signers = vec![config.signers[0]]; (message, signers) }; // Submit transaction 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( rpc_client, - &config.keypair.pubkey(), + &config.signers[0].pubkey(), &fee_calculator, &tx.message, )?; diff --git a/cli/src/vote.rs b/cli/src/vote.rs index 167477117f..33c1c6db40 100644 --- a/cli/src/vote.rs +++ b/cli/src/vote.rs @@ -1,15 +1,16 @@ use crate::cli::{ - build_balance_message, check_account_for_fee, check_unique_pubkeys, - log_instruction_custom_error, CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult, + build_balance_message, check_account_for_fee, check_unique_pubkeys, generate_unique_signers, + log_instruction_custom_error, CliCommand, CliCommandInfo, CliConfig, CliError, CliSignerInfo, + ProcessResult, }; use clap::{value_t_or_exit, App, Arg, ArgMatches, SubCommand}; use solana_clap_utils::{input_parsers::*, input_validators::*}; use solana_client::rpc_client::RpcClient; +use solana_remote_wallet::remote_wallet::RemoteWalletManager; use solana_sdk::{ account::Account, + message::Message, pubkey::Pubkey, - signature::Keypair, - signature::Signer, system_instruction::{create_address_with_seed, SystemError}, transaction::Transaction, }; @@ -17,6 +18,7 @@ use solana_vote_program::{ vote_instruction::{self, VoteError}, vote_state::{VoteAuthorize, VoteInit, VoteState}, }; +use std::sync::Arc; pub trait VoteSubCommands { fn vote_subcommands(self) -> Self; @@ -175,56 +177,88 @@ impl VoteSubCommands for App<'_, '_> { } } -pub fn parse_vote_create_account(matches: &ArgMatches<'_>) -> Result { - let vote_account = keypair_of(matches, "vote_account").unwrap(); +pub fn parse_vote_create_account( + matches: &ArgMatches<'_>, + default_signer_path: &str, + wallet_manager: Option<&Arc>, +) -> Result { + let (vote_account, _) = signer_of(matches, "vote_account", wallet_manager)?; let seed = matches.value_of("seed").map(|s| s.to_string()); let identity_pubkey = pubkey_of(matches, "identity_pubkey").unwrap(); let commission = value_t_or_exit!(matches, "commission", u8); let authorized_voter = pubkey_of(matches, "authorized_voter"); 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 { command: CliCommand::CreateVoteAccount { - vote_account: vote_account.into(), seed, node_pubkey: identity_pubkey, authorized_voter, authorized_withdrawer, commission, }, - require_keypair: true, + signers, }) } pub fn parse_vote_authorize( matches: &ArgMatches<'_>, + default_signer_path: &str, + wallet_manager: Option<&Arc>, vote_authorize: VoteAuthorize, ) -> Result { let vote_account_pubkey = pubkey_of(matches, "vote_account_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 { command: CliCommand::VoteAuthorize { vote_account_pubkey, new_authorized_pubkey, vote_authorize, }, - require_keypair: true, + signers, }) } -pub fn parse_vote_update_validator(matches: &ArgMatches<'_>) -> Result { +pub fn parse_vote_update_validator( + matches: &ArgMatches<'_>, + default_signer_path: &str, + wallet_manager: Option<&Arc>, +) -> Result { let vote_account_pubkey = pubkey_of(matches, "vote_account_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 { command: CliCommand::VoteUpdateValidator { vote_account_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, use_lamports_unit, }, - require_keypair: false, + signers: vec![], }) } pub fn process_create_vote_account( rpc_client: &RpcClient, config: &CliConfig, - vote_account: &Keypair, seed: &Option, identity_pubkey: &Pubkey, authorized_voter: &Option, authorized_withdrawer: &Option, commission: u8, ) -> ProcessResult { + let vote_account = config.signers[1]; let vote_account_pubkey = vote_account.pubkey(); let vote_account_address = if let Some(seed) = seed { 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 }; 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()), )?; @@ -293,16 +327,16 @@ pub fn process_create_vote_account( let ixs = if let Some(seed) = seed { vote_instruction::create_account_with_seed( - &config.keypair.pubkey(), // from - &vote_account_address, // to - &vote_account_pubkey, // base - seed, // seed + &config.signers[0].pubkey(), // from + &vote_account_address, // to + &vote_account_pubkey, // base + seed, // seed &vote_init, required_balance, ) } else { vote_instruction::create_account( - &config.keypair.pubkey(), + &config.signers[0].pubkey(), &vote_account_pubkey, &vote_init, required_balance, @@ -310,20 +344,16 @@ pub fn process_create_vote_account( }; let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; - let signers = if vote_account_pubkey != config.keypair.pubkey() { - vec![&config.keypair, vote_account] // both must sign if `from` and `to` differ - } else { - 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); + let message = Message::new(ixs); + let mut tx = Transaction::new_unsigned(message); + tx.try_sign(&config.signers, recent_blockhash)?; check_account_for_fee( rpc_client, - &config.keypair.pubkey(), + &config.signers[0].pubkey(), &fee_calculator, &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::(result) } @@ -340,25 +370,22 @@ pub fn process_vote_authorize( )?; let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; let ixs = vec![vote_instruction::authorize( - vote_account_pubkey, // vote account to update - &config.keypair.pubkey(), // current authorized voter - new_authorized_pubkey, // new vote signer/withdrawer - vote_authorize, // vote or withdraw + vote_account_pubkey, // vote account to update + &config.signers[0].pubkey(), // current authorized voter + new_authorized_pubkey, // new vote signer/withdrawer + vote_authorize, // vote or withdraw )]; - let mut tx = Transaction::new_signed_with_payer( - ixs, - Some(&config.keypair.pubkey()), - &[&config.keypair], - recent_blockhash, - ); + let message = Message::new_with_payer(ixs, Some(&config.signers[0].pubkey())); + let mut tx = Transaction::new_unsigned(message); + tx.try_sign(&config.signers, recent_blockhash)?; check_account_for_fee( rpc_client, - &config.keypair.pubkey(), + &config.signers[0].pubkey(), &fee_calculator, &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::(result) } @@ -367,8 +394,8 @@ pub fn process_vote_update_validator( config: &CliConfig, vote_account_pubkey: &Pubkey, new_identity_pubkey: &Pubkey, - authorized_voter: &Keypair, ) -> ProcessResult { + let authorized_voter = config.signers[1]; check_unique_pubkeys( (vote_account_pubkey, "vote_account_pubkey".to_string()), (new_identity_pubkey, "new_identity_pubkey".to_string()), @@ -380,19 +407,16 @@ pub fn process_vote_update_validator( new_identity_pubkey, )]; - let mut tx = Transaction::new_signed_with_payer( - ixs, - Some(&config.keypair.pubkey()), - &[&config.keypair, authorized_voter], - recent_blockhash, - ); + let message = Message::new_with_payer(ixs, Some(&config.signers[0].pubkey())); + let mut tx = Transaction::new_unsigned(message); + tx.try_sign(&config.signers, recent_blockhash)?; check_account_for_fee( rpc_client, - &config.keypair.pubkey(), + &config.signers[0].pubkey(), &fee_calculator, &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::(result) } @@ -474,7 +498,7 @@ pub fn process_show_vote_account( mod tests { use super::*; 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; fn make_tmp_file() -> (String, NamedTempFile) { @@ -492,6 +516,10 @@ mod tests { let pubkey2 = keypair2.pubkey(); 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![ "test", "vote-authorize-voter", @@ -499,14 +527,14 @@ mod tests { &pubkey2_string, ]); assert_eq!( - parse_command(&test_authorize_voter).unwrap(), + parse_command(&test_authorize_voter, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::VoteAuthorize { vote_account_pubkey: pubkey, new_authorized_pubkey: pubkey2, vote_authorize: VoteAuthorize::Voter }, - require_keypair: true + signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()], } ); @@ -525,17 +553,19 @@ mod tests { "10", ]); assert_eq!( - parse_command(&test_create_vote_account).unwrap(), + parse_command(&test_create_vote_account, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::CreateVoteAccount { - vote_account: keypair.into(), seed: None, node_pubkey, authorized_voter: None, authorized_withdrawer: None, 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, ]); assert_eq!( - parse_command(&test_create_vote_account2).unwrap(), + parse_command(&test_create_vote_account2, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::CreateVoteAccount { - vote_account: keypair.into(), seed: None, node_pubkey, authorized_voter: None, authorized_withdrawer: None, 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(), ]); assert_eq!( - parse_command(&test_create_vote_account3).unwrap(), + parse_command(&test_create_vote_account3, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::CreateVoteAccount { - vote_account: keypair.into(), seed: None, node_pubkey, authorized_voter: Some(authed), authorized_withdrawer: None, 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(), ]); assert_eq!( - parse_command(&test_create_vote_account4).unwrap(), + parse_command(&test_create_vote_account4, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::CreateVoteAccount { - vote_account: keypair.into(), seed: None, node_pubkey, authorized_voter: None, authorized_withdrawer: Some(authed), 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, ]); assert_eq!( - parse_command(&test_update_validator).unwrap(), + parse_command(&test_update_validator, &default_keypair_file, None).unwrap(), CliCommandInfo { command: CliCommand::VoteUpdateValidator { vote_account_pubkey: pubkey, 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()) + ], } ); } diff --git a/cli/tests/deploy.rs b/cli/tests/deploy.rs index 58c3c8f731..66f9bc9df0 100644 --- a/cli/tests/deploy.rs +++ b/cli/tests/deploy.rs @@ -3,7 +3,7 @@ use solana_cli::cli::{process_command, CliCommand, CliConfig}; use solana_client::rpc_client::RpcClient; use solana_core::validator::new_validator_for_tests; 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::{ fs::{remove_dir_all, File}, io::Read, @@ -38,6 +38,7 @@ fn test_cli_deploy_program() { .unwrap(); let mut config = CliConfig::default(); + let keypair = Keypair::new(); config.json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); config.command = CliCommand::Airdrop { faucet_host: None, @@ -45,6 +46,7 @@ fn test_cli_deploy_program() { pubkey: None, lamports: minimum_balance_for_rent_exemption + 1, // min balance for rent exemption + leftover for tx processing }; + config.signers = vec![&keypair]; process_command(&config).unwrap(); config.command = CliCommand::Deploy(pathbuf.to_str().unwrap().to_string()); diff --git a/cli/tests/nonce.rs b/cli/tests/nonce.rs index f5f89487f1..b5efc7a36e 100644 --- a/cli/tests/nonce.rs +++ b/cli/tests/nonce.rs @@ -1,29 +1,17 @@ -use solana_cli::cli::{ - process_command, request_and_confirm_airdrop, CliCommand, CliConfig, SigningAuthority, -}; +use solana_cli::cli::{process_command, request_and_confirm_airdrop, CliCommand, CliConfig}; use solana_client::rpc_client::RpcClient; use solana_faucet::faucet::run_local_faucet; use solana_sdk::{ hash::Hash, pubkey::Pubkey, - signature::{read_keypair_file, write_keypair, Keypair, Signer}, + signature::{keypair_from_seed, Keypair, Signer}, system_instruction::create_address_with_seed, system_program, }; -use std::fs::remove_dir_all; -use std::sync::mpsc::channel; +use std::{fs::remove_dir_all, sync::mpsc::channel, thread::sleep, time::Duration}; #[cfg(test)] 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) { (0..5).for_each(|tries| { @@ -46,26 +34,9 @@ fn test_nonce() { let faucet_addr = receiver.recv().unwrap(); 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( - &rpc_client, - &faucet_addr, - &mut config_payer, - &mut config_nonce, - &keypair_file, - None, - None, - ); + full_battery_tests(&rpc_client, &faucet_addr, json_rpc_url, None, false); server.close().unwrap(); remove_dir_all(ledger_path).unwrap(); @@ -79,25 +50,14 @@ fn test_nonce_with_seed() { let faucet_addr = receiver.recv().unwrap(); let rpc_client = RpcClient::new_socket(leader_data.rpc); - - 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(); + let json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); full_battery_tests( &rpc_client, &faucet_addr, - &mut config_payer, - &mut config_nonce, - &keypair_file, + json_rpc_url, Some(String::from("seed")), - None, + false, ); server.close().unwrap(); @@ -112,78 +72,73 @@ fn test_nonce_with_authority() { let faucet_addr = receiver.recv().unwrap(); 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 (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), - ); + full_battery_tests(&rpc_client, &faucet_addr, json_rpc_url, None, true); server.close().unwrap(); remove_dir_all(ledger_path).unwrap(); } -fn read_keypair_from_option(keypair_file: &Option<&str>) -> Option { - keypair_file.map(|akf| read_keypair_file(&akf).unwrap().into()) -} - fn full_battery_tests( rpc_client: &RpcClient, faucet_addr: &std::net::SocketAddr, - config_payer: &mut CliConfig, - config_nonce: &mut CliConfig, - nonce_keypair_file: &str, + json_rpc_url: String, seed: Option, - 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( &rpc_client, &faucet_addr, - &config_payer.keypair.pubkey(), + &config_payer.signers[0].pubkey(), 2000, ) .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() { - create_address_with_seed(&config_nonce.keypair.pubkey(), seed, &system_program::id()) - .unwrap() + create_address_with_seed( + &config_nonce.signers[0].pubkey(), + seed, + &system_program::id(), + ) + .unwrap() } 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 + config_payer.signers.push(&nonce_keypair); config_payer.command = CliCommand::CreateNonceAccount { - nonce_account: read_keypair_file(&nonce_keypair_file).unwrap().into(), + nonce_account: 1, seed, - nonce_authority: read_keypair_from_option(&authority_keypair_file) - .map(|na: SigningAuthority| na.pubkey()), + nonce_authority: optional_authority, lamports: 1000, }; 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); // Get nonce + config_payer.signers.pop(); config_payer.command = CliCommand::GetNonce(nonce_account); let first_nonce_string = process_command(&config_payer).unwrap(); let first_nonce = first_nonce_string.parse::().unwrap(); @@ -195,14 +150,24 @@ fn full_battery_tests( 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 + config_payer.signers = authorized_signers.clone(); config_payer.command = CliCommand::NewNonce { nonce_account, - nonce_authority: read_keypair_from_option(&authority_keypair_file), + nonce_authority: index, }; process_command(&config_payer).unwrap(); // Get nonce + config_payer.signers = vec![&payer]; config_payer.command = CliCommand::GetNonce(nonce_account); let third_nonce_string = process_command(&config_payer).unwrap(); let third_nonce = third_nonce_string.parse::().unwrap(); @@ -211,14 +176,15 @@ fn full_battery_tests( // Withdraw from nonce account let payee_pubkey = Pubkey::new_rand(); + config_payer.signers = authorized_signers; config_payer.command = CliCommand::WithdrawFromNonceAccount { nonce_account, - nonce_authority: read_keypair_from_option(&authority_keypair_file), + nonce_authority: index, destination_account_pubkey: payee_pubkey, lamports: 100, }; 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(100, &rpc_client, &payee_pubkey); @@ -231,48 +197,37 @@ fn full_battery_tests( // Set new authority 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 { nonce_account, - nonce_authority: read_keypair_from_option(&authority_keypair_file), - new_authority: read_keypair_file(&new_authority_keypair_file) - .unwrap() - .pubkey(), + nonce_authority: index, + new_authority: new_authority.pubkey(), }; process_command(&config_payer).unwrap(); // Old authority fails now config_payer.command = CliCommand::NewNonce { nonce_account, - nonce_authority: read_keypair_from_option(&authority_keypair_file), + nonce_authority: index, }; process_command(&config_payer).unwrap_err(); // New authority can advance nonce + config_payer.signers = vec![&payer, &new_authority]; config_payer.command = CliCommand::NewNonce { nonce_account, - nonce_authority: Some( - read_keypair_file(&new_authority_keypair_file) - .unwrap() - .into(), - ), + nonce_authority: 1, }; process_command(&config_payer).unwrap(); // New authority can withdraw from nonce account config_payer.command = CliCommand::WithdrawFromNonceAccount { nonce_account, - nonce_authority: Some( - read_keypair_file(&new_authority_keypair_file) - .unwrap() - .into(), - ), + nonce_authority: 1, destination_account_pubkey: payee_pubkey, lamports: 100, }; 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(200, &rpc_client, &payee_pubkey); } diff --git a/cli/tests/pay.rs b/cli/tests/pay.rs index 9386c38c1c..2d30c6f119 100644 --- a/cli/tests/pay.rs +++ b/cli/tests/pay.rs @@ -1,5 +1,6 @@ use chrono::prelude::*; use serde_json::Value; +use solana_clap_utils::keypair::presigner_from_pubkey_sigs; use solana_cli::{ cli::{process_command, request_and_confirm_airdrop, CliCommand, CliConfig, PayCommand}, offline::{parse_sign_only_reply_string, BlockhashQuery}, @@ -11,21 +12,12 @@ use solana_sdk::{ fee_calculator::FeeCalculator, nonce_state::NonceState, pubkey::Pubkey, - signature::{read_keypair_file, write_keypair, Keypair, Signer}, + signature::{Keypair, Signer}, }; -use std::fs::remove_dir_all; -use std::sync::mpsc::channel; +use std::{fs::remove_dir_all, sync::mpsc::channel, thread::sleep, time::Duration}; #[cfg(test)] 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) { (0..5).for_each(|tries| { @@ -50,32 +42,36 @@ fn test_cli_timestamp_tx() { let faucet_addr = receiver.recv().unwrap(); 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(); config_payer.json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); + config_payer.signers = vec![&default_signer0]; let mut config_witness = CliConfig::default(); config_witness.json_rpc_url = config_payer.json_rpc_url.clone(); + config_witness.signers = vec![&default_signer1]; assert_ne!( - config_payer.keypair.pubkey(), - config_witness.keypair.pubkey() + config_payer.signers[0].pubkey(), + config_witness.signers[0].pubkey() ); request_and_confirm_airdrop( &rpc_client, &faucet_addr, - &config_payer.keypair.pubkey(), + &config_payer.signers[0].pubkey(), 50, ) .unwrap(); - check_balance(50, &rpc_client, &config_payer.keypair.pubkey()); + check_balance(50, &rpc_client, &config_payer.signers[0].pubkey()); request_and_confirm_airdrop( &rpc_client, &faucet_addr, - &config_witness.keypair.pubkey(), + &config_witness.signers[0].pubkey(), 1, ) .unwrap(); @@ -87,7 +83,7 @@ fn test_cli_timestamp_tx() { lamports: 10, to: bob_pubkey, timestamp: Some(dt), - timestamp_pubkey: Some(config_witness.keypair.pubkey()), + timestamp_pubkey: Some(config_witness.signers[0].pubkey()), ..PayCommand::default() }); let sig_response = process_command(&config_payer); @@ -99,7 +95,7 @@ fn test_cli_timestamp_tx() { .expect("base58-encoded public key"); 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(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); 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(10, &rpc_client, &bob_pubkey); // recipient balance @@ -125,30 +121,34 @@ fn test_cli_witness_tx() { let faucet_addr = receiver.recv().unwrap(); 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(); config_payer.json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); + config_payer.signers = vec![&default_signer0]; let mut config_witness = CliConfig::default(); config_witness.json_rpc_url = config_payer.json_rpc_url.clone(); + config_witness.signers = vec![&default_signer1]; assert_ne!( - config_payer.keypair.pubkey(), - config_witness.keypair.pubkey() + config_payer.signers[0].pubkey(), + config_witness.signers[0].pubkey() ); request_and_confirm_airdrop( &rpc_client, &faucet_addr, - &config_payer.keypair.pubkey(), + &config_payer.signers[0].pubkey(), 50, ) .unwrap(); request_and_confirm_airdrop( &rpc_client, &faucet_addr, - &config_witness.keypair.pubkey(), + &config_witness.signers[0].pubkey(), 1, ) .unwrap(); @@ -157,7 +157,7 @@ fn test_cli_witness_tx() { config_payer.command = CliCommand::Pay(PayCommand { lamports: 10, to: bob_pubkey, - witnesses: Some(vec![config_witness.keypair.pubkey()]), + witnesses: Some(vec![config_witness.signers[0].pubkey()]), ..PayCommand::default() }); let sig_response = process_command(&config_payer); @@ -169,7 +169,7 @@ fn test_cli_witness_tx() { .expect("base58-encoded public key"); 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(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); 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(10, &rpc_client, &bob_pubkey); // recipient balance @@ -195,23 +195,27 @@ fn test_cli_cancel_tx() { let faucet_addr = receiver.recv().unwrap(); 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(); config_payer.json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); + config_payer.signers = vec![&default_signer0]; let mut config_witness = CliConfig::default(); config_witness.json_rpc_url = config_payer.json_rpc_url.clone(); + config_witness.signers = vec![&default_signer1]; assert_ne!( - config_payer.keypair.pubkey(), - config_witness.keypair.pubkey() + config_payer.signers[0].pubkey(), + config_witness.signers[0].pubkey() ); request_and_confirm_airdrop( &rpc_client, &faucet_addr, - &config_payer.keypair.pubkey(), + &config_payer.signers[0].pubkey(), 50, ) .unwrap(); @@ -220,7 +224,7 @@ fn test_cli_cancel_tx() { config_payer.command = CliCommand::Pay(PayCommand { lamports: 10, to: bob_pubkey, - witnesses: Some(vec![config_witness.keypair.pubkey()]), + witnesses: Some(vec![config_witness.signers[0].pubkey()]), cancelable: true, ..PayCommand::default() }); @@ -233,7 +237,7 @@ fn test_cli_cancel_tx() { .expect("base58-encoded public key"); 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(0, &rpc_client, &bob_pubkey); // recipient balance @@ -241,7 +245,7 @@ fn test_cli_cancel_tx() { config_payer.command = CliCommand::Cancel(process_id); 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, &bob_pubkey); // recipient balance @@ -259,22 +263,26 @@ fn test_offline_pay_tx() { let faucet_addr = receiver.recv().unwrap(); 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(); config_offline.json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); + config_offline.signers = vec![&default_offline_signer]; let mut config_online = CliConfig::default(); config_online.json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); + config_online.signers = vec![&default_signer]; assert_ne!( - config_offline.keypair.pubkey(), - config_online.keypair.pubkey() + config_offline.signers[0].pubkey(), + config_online.signers[0].pubkey() ); request_and_confirm_airdrop( &rpc_client, &faucet_addr, - &config_offline.keypair.pubkey(), + &config_offline.signers[0].pubkey(), 50, ) .unwrap(); @@ -282,12 +290,12 @@ fn test_offline_pay_tx() { request_and_confirm_airdrop( &rpc_client, &faucet_addr, - &config_online.keypair.pubkey(), + &config_online.signers[0].pubkey(), 50, ) .unwrap(); - check_balance(50, &rpc_client, &config_offline.keypair.pubkey()); - check_balance(50, &rpc_client, &config_online.keypair.pubkey()); + check_balance(50, &rpc_client, &config_offline.signers[0].pubkey()); + check_balance(50, &rpc_client, &config_online.signers[0].pubkey()); let (blockhash, _) = rpc_client.get_recent_blockhash().unwrap(); config_offline.command = CliCommand::Pay(PayCommand { @@ -299,22 +307,25 @@ fn test_offline_pay_tx() { }); let sig_response = process_command(&config_offline).unwrap(); - check_balance(50, &rpc_client, &config_offline.keypair.pubkey()); - check_balance(50, &rpc_client, &config_online.keypair.pubkey()); + check_balance(50, &rpc_client, &config_offline.signers[0].pubkey()); + check_balance(50, &rpc_client, &config_online.signers[0].pubkey()); check_balance(0, &rpc_client, &bob_pubkey); 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 { lamports: 10, to: bob_pubkey, - signers: Some(signers), blockhash_query: BlockhashQuery::FeeCalculator(blockhash), ..PayCommand::default() }); process_command(&config_online).unwrap(); - check_balance(40, &rpc_client, &config_offline.keypair.pubkey()); - check_balance(50, &rpc_client, &config_online.keypair.pubkey()); + check_balance(40, &rpc_client, &config_offline.signers[0].pubkey()); + check_balance(50, &rpc_client, &online_pubkey); check_balance(10, &rpc_client, &bob_pubkey); server.close().unwrap(); @@ -331,9 +342,11 @@ fn test_nonced_pay_tx() { let faucet_addr = receiver.recv().unwrap(); let rpc_client = RpcClient::new_socket(leader_data.rpc); + let default_signer = Keypair::new(); let mut config = CliConfig::default(); 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 .get_minimum_balance_for_rent_exemption(NonceState::size()) @@ -342,29 +355,28 @@ fn test_nonced_pay_tx() { request_and_confirm_airdrop( &rpc_client, &faucet_addr, - &config.keypair.pubkey(), + &config.signers[0].pubkey(), 50 + minimum_nonce_balance, ) .unwrap(); check_balance( 50 + minimum_nonce_balance, &rpc_client, - &config.keypair.pubkey(), + &config.signers[0].pubkey(), ); // Create nonce account 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 { - nonce_account: read_keypair_file(&nonce_keypair_file).unwrap().into(), + nonce_account: 1, seed: None, - nonce_authority: Some(config.keypair.pubkey()), + nonce_authority: Some(config.signers[0].pubkey()), lamports: minimum_nonce_balance, }; + config.signers.push(&nonce_account); 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()); // Fetch nonce hash @@ -376,6 +388,7 @@ fn test_nonced_pay_tx() { }; let bob_pubkey = Pubkey::new_rand(); + config.signers = vec![&default_signer]; config.command = CliCommand::Pay(PayCommand { lamports: 10, to: bob_pubkey, @@ -385,7 +398,7 @@ fn test_nonced_pay_tx() { }); 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); // Verify that nonce has been used diff --git a/cli/tests/request_airdrop.rs b/cli/tests/request_airdrop.rs index 931054bf07..ca92a60662 100644 --- a/cli/tests/request_airdrop.rs +++ b/cli/tests/request_airdrop.rs @@ -2,9 +2,8 @@ use solana_cli::cli::{process_command, CliCommand, CliConfig}; use solana_client::rpc_client::RpcClient; use solana_core::validator::new_validator_for_tests; use solana_faucet::faucet::run_local_faucet; -use solana_sdk::signature::Signer; -use std::fs::remove_dir_all; -use std::sync::mpsc::channel; +use solana_sdk::signature::Keypair; +use std::{fs::remove_dir_all, sync::mpsc::channel}; #[test] fn test_cli_request_airdrop() { @@ -21,6 +20,8 @@ fn test_cli_request_airdrop() { pubkey: None, lamports: 50, }; + let keypair = Keypair::new(); + bob_config.signers = vec![&keypair]; let sig_response = process_command(&bob_config); sig_response.unwrap(); @@ -28,7 +29,7 @@ fn test_cli_request_airdrop() { let rpc_client = RpcClient::new_socket(leader_data.rpc); let balance = rpc_client - .retry_get_balance(&bob_config.keypair.pubkey(), 1) + .retry_get_balance(&bob_config.signers[0].pubkey(), 1) .unwrap() .unwrap(); assert_eq!(balance, 50); diff --git a/cli/tests/stake.rs b/cli/tests/stake.rs index bb75922442..d2de68ed80 100644 --- a/cli/tests/stake.rs +++ b/cli/tests/stake.rs @@ -1,3 +1,4 @@ +use solana_clap_utils::keypair::presigner_from_pubkey_sigs; use solana_cli::{ cli::{process_command, request_and_confirm_airdrop, CliCommand, CliConfig}, offline::{parse_sign_only_reply_string, BlockhashQuery}, @@ -9,26 +10,16 @@ use solana_sdk::{ fee_calculator::FeeCalculator, nonce_state::NonceState, pubkey::Pubkey, - signature::{keypair_from_seed, read_keypair_file, write_keypair, Keypair, Signer}, + signature::{keypair_from_seed, Keypair, Signer}, system_instruction::create_address_with_seed, }; use solana_stake_program::stake_state::{Lockup, StakeAuthorize, StakeState}; -use std::fs::remove_dir_all; -use std::sync::mpsc::channel; +use std::{fs::remove_dir_all, sync::mpsc::channel, thread::sleep, time::Duration}; #[cfg(test)] use solana_core::validator::{ new_validator_for_tests, new_validator_for_tests_ex, new_validator_for_tests_with_vote_pubkey, }; -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) { (0..5).for_each(|tries| { @@ -51,21 +42,26 @@ fn test_stake_delegation_force() { let faucet_addr = receiver.recv().unwrap(); let rpc_client = RpcClient::new_socket(leader_data.rpc); + let default_signer = Keypair::new(); let mut config = CliConfig::default(); config.json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); + config.signers = vec![&default_signer]; - request_and_confirm_airdrop(&rpc_client, &faucet_addr, &config.keypair.pubkey(), 100_000) - .unwrap(); + request_and_confirm_airdrop( + &rpc_client, + &faucet_addr, + &config.signers[0].pubkey(), + 100_000, + ) + .unwrap(); // Create vote account let vote_keypair = Keypair::new(); - let (vote_keypair_file, mut tmp_file) = make_tmp_file(); - write_keypair(&vote_keypair, tmp_file.as_file_mut()).unwrap(); + config.signers = vec![&default_signer, &vote_keypair]; config.command = CliCommand::CreateVoteAccount { - vote_account: read_keypair_file(&vote_keypair_file).unwrap().into(), seed: None, - node_pubkey: config.keypair.pubkey(), + node_pubkey: config.signers[0].pubkey(), authorized_voter: None, authorized_withdrawer: None, commission: 0, @@ -74,37 +70,35 @@ fn test_stake_delegation_force() { // Create stake account let stake_keypair = Keypair::new(); - let (stake_keypair_file, mut tmp_file) = make_tmp_file(); - write_keypair(&stake_keypair, tmp_file.as_file_mut()).unwrap(); + config.signers = vec![&default_signer, &stake_keypair]; config.command = CliCommand::CreateStakeAccount { - stake_account: read_keypair_file(&stake_keypair_file).unwrap().into(), + stake_account: 1, seed: None, staker: None, withdrawer: None, lockup: Lockup::default(), lamports: 50_000, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::All, nonce_account: None, - nonce_authority: None, - fee_payer: None, - from: None, + nonce_authority: 0, + fee_payer: 0, + from: 0, }; process_command(&config).unwrap(); // Delegate stake fails (vote account had never voted) + config.signers = vec![&default_signer]; config.command = CliCommand::DelegateStake { stake_account_pubkey: stake_keypair.pubkey(), vote_account_pubkey: vote_keypair.pubkey(), - stake_authority: None, + stake_authority: 0, force: false, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::default(), nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }; process_command(&config).unwrap_err(); @@ -112,14 +106,13 @@ fn test_stake_delegation_force() { config.command = CliCommand::DelegateStake { stake_account_pubkey: stake_keypair.pubkey(), vote_account_pubkey: vote_keypair.pubkey(), - stake_authority: None, + stake_authority: 0, force: true, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::default(), nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }; process_command(&config).unwrap(); @@ -139,28 +132,23 @@ fn test_seed_stake_delegation_and_deactivation() { let rpc_client = RpcClient::new_socket(leader_data.rpc); + let validator_keypair = keypair_from_seed(&[0u8; 32]).unwrap(); let mut config_validator = CliConfig::default(); config_validator.json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); - - let (validator_keypair_file, mut tmp_file) = make_tmp_file(); - write_keypair(&config_validator.keypair, tmp_file.as_file_mut()).unwrap(); - - let mut config_stake = CliConfig::default(); - config_stake.json_rpc_url = - format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); + config_validator.signers = vec![&validator_keypair]; request_and_confirm_airdrop( &rpc_client, &faucet_addr, - &config_validator.keypair.pubkey(), + &config_validator.signers[0].pubkey(), 100_000, ) .unwrap(); - check_balance(100_000, &rpc_client, &config_validator.keypair.pubkey()); + check_balance(100_000, &rpc_client, &config_validator.signers[0].pubkey()); let stake_address = create_address_with_seed( - &config_validator.keypair.pubkey(), + &config_validator.signers[0].pubkey(), "hi there", &solana_stake_program::id(), ) @@ -169,19 +157,18 @@ fn test_seed_stake_delegation_and_deactivation() { // Create stake account with a seed, uses the validator config as the base, // which is nice ;) config_validator.command = CliCommand::CreateStakeAccount { - stake_account: read_keypair_file(&validator_keypair_file).unwrap().into(), + stake_account: 0, seed: Some("hi there".to_string()), staker: None, withdrawer: None, lockup: Lockup::default(), lamports: 50_000, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::All, nonce_account: None, - nonce_authority: None, - fee_payer: None, - from: None, + nonce_authority: 0, + fee_payer: 0, + from: 0, }; process_command(&config_validator).unwrap(); @@ -189,27 +176,25 @@ fn test_seed_stake_delegation_and_deactivation() { config_validator.command = CliCommand::DelegateStake { stake_account_pubkey: stake_address, vote_account_pubkey: vote_pubkey, - stake_authority: None, + stake_authority: 0, force: false, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::default(), nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }; process_command(&config_validator).unwrap(); // Deactivate stake config_validator.command = CliCommand::DeactivateStake { stake_account_pubkey: stake_address, - stake_authority: None, + stake_authority: 0, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::default(), nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }; process_command(&config_validator).unwrap(); @@ -228,69 +213,66 @@ fn test_stake_delegation_and_deactivation() { let faucet_addr = receiver.recv().unwrap(); let rpc_client = RpcClient::new_socket(leader_data.rpc); + let validator_keypair = Keypair::new(); let mut config_validator = CliConfig::default(); config_validator.json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); + config_validator.signers = vec![&validator_keypair]; - let mut config_stake = CliConfig::default(); - config_stake.json_rpc_url = - format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); - let (stake_keypair_file, mut tmp_file) = make_tmp_file(); - write_keypair(&config_stake.keypair, tmp_file.as_file_mut()).unwrap(); + let stake_keypair = keypair_from_seed(&[0u8; 32]).unwrap(); request_and_confirm_airdrop( &rpc_client, &faucet_addr, - &config_validator.keypair.pubkey(), + &config_validator.signers[0].pubkey(), 100_000, ) .unwrap(); - check_balance(100_000, &rpc_client, &config_validator.keypair.pubkey()); + check_balance(100_000, &rpc_client, &config_validator.signers[0].pubkey()); // Create stake account + config_validator.signers.push(&stake_keypair); config_validator.command = CliCommand::CreateStakeAccount { - stake_account: read_keypair_file(&stake_keypair_file).unwrap().into(), + stake_account: 1, seed: None, staker: None, withdrawer: None, lockup: Lockup::default(), lamports: 50_000, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::All, nonce_account: None, - nonce_authority: None, - fee_payer: None, - from: None, + nonce_authority: 0, + fee_payer: 0, + from: 0, }; process_command(&config_validator).unwrap(); // Delegate stake + config_validator.signers.pop(); config_validator.command = CliCommand::DelegateStake { - stake_account_pubkey: config_stake.keypair.pubkey(), + stake_account_pubkey: stake_keypair.pubkey(), vote_account_pubkey: vote_pubkey, - stake_authority: None, + stake_authority: 0, force: false, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::default(), nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }; process_command(&config_validator).unwrap(); // Deactivate stake config_validator.command = CliCommand::DeactivateStake { - stake_account_pubkey: config_stake.keypair.pubkey(), - stake_authority: None, + stake_account_pubkey: stake_keypair.pubkey(), + stake_authority: 0, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::default(), nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }; process_command(&config_validator).unwrap(); @@ -313,112 +295,114 @@ fn test_offline_stake_delegation_and_deactivation() { let mut config_validator = CliConfig::default(); config_validator.json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); + let validator_keypair = Keypair::new(); + config_validator.signers = vec![&validator_keypair]; let mut config_payer = CliConfig::default(); config_payer.json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); - let mut config_stake = CliConfig::default(); - config_stake.json_rpc_url = - format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); - let (stake_keypair_file, mut tmp_file) = make_tmp_file(); - write_keypair(&config_stake.keypair, tmp_file.as_file_mut()).unwrap(); + let stake_keypair = keypair_from_seed(&[0u8; 32]).unwrap(); let mut config_offline = CliConfig::default(); config_offline.json_rpc_url = String::default(); config_offline.command = CliCommand::ClusterVersion; + let offline_keypair = Keypair::new(); + config_offline.signers = vec![&offline_keypair]; // Verfiy that we cannot reach the cluster process_command(&config_offline).unwrap_err(); request_and_confirm_airdrop( &rpc_client, &faucet_addr, - &config_validator.keypair.pubkey(), + &config_validator.signers[0].pubkey(), 100_000, ) .unwrap(); - check_balance(100_000, &rpc_client, &config_validator.keypair.pubkey()); + check_balance(100_000, &rpc_client, &config_validator.signers[0].pubkey()); request_and_confirm_airdrop( &rpc_client, &faucet_addr, - &config_offline.keypair.pubkey(), + &config_offline.signers[0].pubkey(), 100_000, ) .unwrap(); - check_balance(100_000, &rpc_client, &config_offline.keypair.pubkey()); + check_balance(100_000, &rpc_client, &config_offline.signers[0].pubkey()); // Create stake account + config_validator.signers.push(&stake_keypair); config_validator.command = CliCommand::CreateStakeAccount { - stake_account: read_keypair_file(&stake_keypair_file).unwrap().into(), + stake_account: 1, seed: None, - staker: Some(config_offline.keypair.pubkey().into()), + staker: Some(config_offline.signers[0].pubkey().into()), withdrawer: None, lockup: Lockup::default(), lamports: 50_000, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::All, nonce_account: None, - nonce_authority: None, - fee_payer: None, - from: None, + nonce_authority: 0, + fee_payer: 0, + from: 0, }; process_command(&config_validator).unwrap(); // Delegate stake offline let (blockhash, _) = rpc_client.get_recent_blockhash().unwrap(); config_offline.command = CliCommand::DelegateStake { - stake_account_pubkey: config_stake.keypair.pubkey(), + stake_account_pubkey: stake_keypair.pubkey(), vote_account_pubkey: vote_pubkey, - stake_authority: None, + stake_authority: 0, force: false, sign_only: true, - signers: None, blockhash_query: BlockhashQuery::None(blockhash, FeeCalculator::default()), nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }; let sig_response = process_command(&config_offline).unwrap(); let (blockhash, signers) = parse_sign_only_reply_string(&sig_response); + let offline_presigner = + presigner_from_pubkey_sigs(&config_offline.signers[0].pubkey(), &signers).unwrap(); + config_payer.signers = vec![&offline_presigner]; config_payer.command = CliCommand::DelegateStake { - stake_account_pubkey: config_stake.keypair.pubkey(), + stake_account_pubkey: stake_keypair.pubkey(), vote_account_pubkey: vote_pubkey, - stake_authority: Some(config_offline.keypair.pubkey().into()), + stake_authority: 0, force: false, sign_only: false, - signers: Some(signers), blockhash_query: BlockhashQuery::None(blockhash, FeeCalculator::default()), nonce_account: None, - nonce_authority: None, - fee_payer: Some(config_offline.keypair.pubkey().into()), + nonce_authority: 0, + fee_payer: 0, }; process_command(&config_payer).unwrap(); // Deactivate stake offline let (blockhash, _) = rpc_client.get_recent_blockhash().unwrap(); config_offline.command = CliCommand::DeactivateStake { - stake_account_pubkey: config_stake.keypair.pubkey(), - stake_authority: None, + stake_account_pubkey: stake_keypair.pubkey(), + stake_authority: 0, sign_only: true, - signers: None, blockhash_query: BlockhashQuery::None(blockhash, FeeCalculator::default()), nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }; let sig_response = process_command(&config_offline).unwrap(); let (blockhash, signers) = parse_sign_only_reply_string(&sig_response); + let offline_presigner = + presigner_from_pubkey_sigs(&config_offline.signers[0].pubkey(), &signers).unwrap(); + config_payer.signers = vec![&offline_presigner]; config_payer.command = CliCommand::DeactivateStake { - stake_account_pubkey: config_stake.keypair.pubkey(), - stake_authority: Some(config_offline.keypair.pubkey().into()), + stake_account_pubkey: stake_keypair.pubkey(), + stake_authority: 0, sign_only: false, - signers: Some(signers), blockhash_query: BlockhashQuery::FeeCalculator(blockhash), nonce_account: None, - nonce_authority: None, - fee_payer: Some(config_offline.keypair.pubkey().into()), + nonce_authority: 0, + fee_payer: 0, }; process_command(&config_payer).unwrap(); @@ -438,45 +422,49 @@ fn test_nonced_stake_delegation_and_deactivation() { let rpc_client = RpcClient::new_socket(leader_data.rpc); + let config_keypair = keypair_from_seed(&[0u8; 32]).unwrap(); let mut config = CliConfig::default(); + config.signers = vec![&config_keypair]; config.json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); let minimum_nonce_balance = rpc_client .get_minimum_balance_for_rent_exemption(NonceState::size()) .unwrap(); - request_and_confirm_airdrop(&rpc_client, &faucet_addr, &config.keypair.pubkey(), 100_000) - .unwrap(); + request_and_confirm_airdrop( + &rpc_client, + &faucet_addr, + &config.signers[0].pubkey(), + 100_000, + ) + .unwrap(); // Create stake account let stake_keypair = Keypair::new(); - let (stake_keypair_file, mut tmp_file) = make_tmp_file(); - write_keypair(&stake_keypair, tmp_file.as_file_mut()).unwrap(); + config.signers.push(&stake_keypair); config.command = CliCommand::CreateStakeAccount { - stake_account: read_keypair_file(&stake_keypair_file).unwrap().into(), + stake_account: 1, seed: None, staker: None, withdrawer: None, lockup: Lockup::default(), lamports: 50_000, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::All, nonce_account: None, - nonce_authority: None, - fee_payer: None, - from: None, + nonce_authority: 0, + fee_payer: 0, + from: 0, }; process_command(&config).unwrap(); // Create nonce account 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.signers[1] = &nonce_account; config.command = CliCommand::CreateNonceAccount { - nonce_account: read_keypair_file(&nonce_keypair_file).unwrap().into(), + nonce_account: 1, seed: None, - nonce_authority: Some(config.keypair.pubkey()), + nonce_authority: Some(config.signers[0].pubkey()), lamports: minimum_nonce_balance, }; process_command(&config).unwrap(); @@ -490,17 +478,17 @@ fn test_nonced_stake_delegation_and_deactivation() { }; // Delegate stake + config.signers = vec![&config_keypair]; config.command = CliCommand::DelegateStake { stake_account_pubkey: stake_keypair.pubkey(), vote_account_pubkey: vote_pubkey, - stake_authority: None, + stake_authority: 0, force: false, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::None(nonce_hash, FeeCalculator::default()), nonce_account: Some(nonce_account.pubkey()), - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }; process_command(&config).unwrap(); @@ -513,16 +501,14 @@ fn test_nonced_stake_delegation_and_deactivation() { }; // Deactivate stake - let config_keypair = Keypair::from_bytes(&config.keypair.to_bytes()).unwrap(); config.command = CliCommand::DeactivateStake { stake_account_pubkey: stake_keypair.pubkey(), - stake_authority: None, + stake_authority: 0, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::FeeCalculator(nonce_hash), nonce_account: Some(nonce_account.pubkey()), - nonce_authority: Some(config_keypair.into()), - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }; process_command(&config).unwrap(); @@ -540,15 +526,25 @@ fn test_stake_authorize() { let faucet_addr = receiver.recv().unwrap(); let rpc_client = RpcClient::new_socket(leader_data.rpc); + let default_signer = Keypair::new(); let mut config = CliConfig::default(); config.json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); + config.signers = vec![&default_signer]; - request_and_confirm_airdrop(&rpc_client, &faucet_addr, &config.keypair.pubkey(), 100_000) - .unwrap(); + request_and_confirm_airdrop( + &rpc_client, + &faucet_addr, + &config.signers[0].pubkey(), + 100_000, + ) + .unwrap(); + let offline_keypair = keypair_from_seed(&[0u8; 32]).unwrap(); let mut config_offline = CliConfig::default(); + config_offline.signers = vec![&offline_keypair]; config_offline.json_rpc_url = String::default(); + let offline_authority_pubkey = config_offline.signers[0].pubkey(); config_offline.command = CliCommand::ClusterVersion; // Verfiy that we cannot reach the cluster process_command(&config_offline).unwrap_err(); @@ -556,7 +552,7 @@ fn test_stake_authorize() { request_and_confirm_airdrop( &rpc_client, &faucet_addr, - &config_offline.keypair.pubkey(), + &config_offline.signers[0].pubkey(), 100_000, ) .unwrap(); @@ -564,41 +560,37 @@ fn test_stake_authorize() { // Create stake account, identity is authority let stake_keypair = Keypair::new(); let stake_account_pubkey = stake_keypair.pubkey(); - let (stake_keypair_file, mut tmp_file) = make_tmp_file(); - write_keypair(&stake_keypair, tmp_file.as_file_mut()).unwrap(); + config.signers.push(&stake_keypair); config.command = CliCommand::CreateStakeAccount { - stake_account: read_keypair_file(&stake_keypair_file).unwrap().into(), + stake_account: 1, seed: None, staker: None, withdrawer: None, lockup: Lockup::default(), lamports: 50_000, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::All, nonce_account: None, - nonce_authority: None, - fee_payer: None, - from: None, + nonce_authority: 0, + fee_payer: 0, + from: 0, }; process_command(&config).unwrap(); // Assign new online stake authority let online_authority = Keypair::new(); let online_authority_pubkey = online_authority.pubkey(); - let (online_authority_file, mut tmp_file) = make_tmp_file(); - write_keypair(&online_authority, tmp_file.as_file_mut()).unwrap(); + config.signers.pop(); config.command = CliCommand::StakeAuthorize { stake_account_pubkey, new_authorized_pubkey: online_authority_pubkey, stake_authorize: StakeAuthorize::Staker, - authority: None, + authority: 0, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::default(), nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }; process_command(&config).unwrap(); let stake_account = rpc_client.get_account(&stake_account_pubkey).unwrap(); @@ -610,20 +602,17 @@ fn test_stake_authorize() { assert_eq!(current_authority, online_authority_pubkey); // Assign new offline stake authority - let offline_authority_pubkey = config_offline.keypair.pubkey(); - let (offline_authority_file, mut tmp_file) = make_tmp_file(); - write_keypair(&config_offline.keypair, tmp_file.as_file_mut()).unwrap(); + config.signers.push(&online_authority); config.command = CliCommand::StakeAuthorize { stake_account_pubkey, new_authorized_pubkey: offline_authority_pubkey, stake_authorize: StakeAuthorize::Staker, - authority: Some(read_keypair_file(&online_authority_file).unwrap().into()), + authority: 1, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::default(), nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }; process_command(&config).unwrap(); let stake_account = rpc_client.get_account(&stake_account_pubkey).unwrap(); @@ -637,34 +626,33 @@ fn test_stake_authorize() { // Offline assignment of new nonced stake authority let nonced_authority = Keypair::new(); let nonced_authority_pubkey = nonced_authority.pubkey(); - let (nonced_authority_file, mut tmp_file) = make_tmp_file(); - write_keypair(&nonced_authority, tmp_file.as_file_mut()).unwrap(); let (blockhash, _) = rpc_client.get_recent_blockhash().unwrap(); config_offline.command = CliCommand::StakeAuthorize { stake_account_pubkey, new_authorized_pubkey: nonced_authority_pubkey, stake_authorize: StakeAuthorize::Staker, - authority: Some(read_keypair_file(&offline_authority_file).unwrap().into()), + authority: 0, sign_only: true, - signers: None, blockhash_query: BlockhashQuery::None(blockhash, FeeCalculator::default()), nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }; let sign_reply = process_command(&config_offline).unwrap(); let (blockhash, signers) = parse_sign_only_reply_string(&sign_reply); + let offline_presigner = + presigner_from_pubkey_sigs(&offline_authority_pubkey, &signers).unwrap(); + config.signers = vec![&offline_presigner]; config.command = CliCommand::StakeAuthorize { stake_account_pubkey, new_authorized_pubkey: nonced_authority_pubkey, stake_authorize: StakeAuthorize::Staker, - authority: Some(offline_authority_pubkey.into()), + authority: 0, sign_only: false, - signers: Some(signers), blockhash_query: BlockhashQuery::FeeCalculator(blockhash), nonce_account: None, - nonce_authority: None, - fee_payer: Some(offline_authority_pubkey.into()), + nonce_authority: 0, + fee_payer: 0, }; process_command(&config).unwrap(); let stake_account = rpc_client.get_account(&stake_account_pubkey).unwrap(); @@ -680,12 +668,11 @@ fn test_stake_authorize() { .get_minimum_balance_for_rent_exemption(NonceState::size()) .unwrap(); 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.signers = vec![&default_signer, &nonce_account]; config.command = CliCommand::CreateNonceAccount { - nonce_account: read_keypair_file(&nonce_keypair_file).unwrap().into(), + nonce_account: 1, seed: None, - nonce_authority: Some(config_offline.keypair.pubkey()), + nonce_authority: Some(offline_authority_pubkey), lamports: minimum_nonce_balance, }; process_command(&config).unwrap(); @@ -701,34 +688,36 @@ fn test_stake_authorize() { // Nonced assignment of new online stake authority let online_authority = Keypair::new(); let online_authority_pubkey = online_authority.pubkey(); - let (_online_authority_file, mut tmp_file) = make_tmp_file(); - write_keypair(&online_authority, tmp_file.as_file_mut()).unwrap(); + config_offline.signers.push(&nonced_authority); config_offline.command = CliCommand::StakeAuthorize { stake_account_pubkey, new_authorized_pubkey: online_authority_pubkey, stake_authorize: StakeAuthorize::Staker, - authority: Some(read_keypair_file(&nonced_authority_file).unwrap().into()), + authority: 1, sign_only: true, - signers: None, blockhash_query: BlockhashQuery::None(nonce_hash, FeeCalculator::default()), nonce_account: Some(nonce_account.pubkey()), - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }; let sign_reply = process_command(&config_offline).unwrap(); let (blockhash, signers) = parse_sign_only_reply_string(&sign_reply); assert_eq!(blockhash, nonce_hash); + let offline_presigner = + presigner_from_pubkey_sigs(&offline_authority_pubkey, &signers).unwrap(); + let nonced_authority_presigner = + presigner_from_pubkey_sigs(&nonced_authority_pubkey, &signers).unwrap(); + config.signers = vec![&offline_presigner, &nonced_authority_presigner]; config.command = CliCommand::StakeAuthorize { stake_account_pubkey, new_authorized_pubkey: online_authority_pubkey, stake_authorize: StakeAuthorize::Staker, - authority: Some(nonced_authority_pubkey.into()), + authority: 1, sign_only: false, - signers: Some(signers), blockhash_query: BlockhashQuery::FeeCalculator(blockhash), nonce_account: Some(nonce_account.pubkey()), - nonce_authority: Some(offline_authority_pubkey.into()), - fee_payer: Some(offline_authority_pubkey.into()), + nonce_authority: 0, + fee_payer: 0, }; process_command(&config).unwrap(); let stake_account = rpc_client.get_account(&stake_account_pubkey).unwrap(); @@ -762,28 +751,30 @@ fn test_stake_authorize_with_fee_payer() { let faucet_addr = receiver.recv().unwrap(); let rpc_client = RpcClient::new_socket(leader_data.rpc); + let default_signer = Keypair::new(); + let default_pubkey = default_signer.pubkey(); let mut config = CliConfig::default(); config.json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); + config.signers = vec![&default_signer]; + let payer_keypair = keypair_from_seed(&[0u8; 32]).unwrap(); let mut config_payer = CliConfig::default(); + config_payer.signers = vec![&payer_keypair]; config_payer.json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); - let payer_pubkey = config_payer.keypair.pubkey(); - let (payer_keypair_file, mut tmp_file) = make_tmp_file(); - write_keypair(&config_payer.keypair, tmp_file.as_file_mut()).unwrap(); + let payer_pubkey = config_payer.signers[0].pubkey(); let mut config_offline = CliConfig::default(); - let offline_pubkey = config_offline.keypair.pubkey(); - let (_offline_keypair_file, mut tmp_file) = make_tmp_file(); - write_keypair(&config_offline.keypair, tmp_file.as_file_mut()).unwrap(); + let offline_signer = Keypair::new(); + config_offline.signers = vec![&offline_signer]; + let offline_pubkey = config_offline.signers[0].pubkey(); // Verify we're offline config_offline.command = CliCommand::ClusterVersion; process_command(&config_offline).unwrap_err(); - request_and_confirm_airdrop(&rpc_client, &faucet_addr, &config.keypair.pubkey(), 100_000) - .unwrap(); - check_balance(100_000, &rpc_client, &config.keypair.pubkey()); + request_and_confirm_airdrop(&rpc_client, &faucet_addr, &default_pubkey, 100_000).unwrap(); + check_balance(100_000, &rpc_client, &config.signers[0].pubkey()); request_and_confirm_airdrop(&rpc_client, &faucet_addr, &payer_pubkey, 100_000).unwrap(); check_balance(100_000, &rpc_client, &payer_pubkey); @@ -794,58 +785,44 @@ fn test_stake_authorize_with_fee_payer() { // Create stake account, identity is authority let stake_keypair = Keypair::new(); let stake_account_pubkey = stake_keypair.pubkey(); - let (stake_keypair_file, mut tmp_file) = make_tmp_file(); - write_keypair(&stake_keypair, tmp_file.as_file_mut()).unwrap(); + config.signers.push(&stake_keypair); config.command = CliCommand::CreateStakeAccount { - stake_account: read_keypair_file(&stake_keypair_file).unwrap().into(), + stake_account: 1, seed: None, staker: None, withdrawer: None, lockup: Lockup::default(), lamports: 50_000, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::All, nonce_account: None, - nonce_authority: None, - fee_payer: None, - from: None, + nonce_authority: 0, + fee_payer: 0, + from: 0, }; process_command(&config).unwrap(); // `config` balance should be 50,000 - 1 stake account sig - 1 fee sig - check_balance( - 50_000 - SIG_FEE - SIG_FEE, - &rpc_client, - &config.keypair.pubkey(), - ); + check_balance(50_000 - SIG_FEE - SIG_FEE, &rpc_client, &default_pubkey); // Assign authority with separate fee payer + config.signers = vec![&default_signer, &payer_keypair]; config.command = CliCommand::StakeAuthorize { stake_account_pubkey, new_authorized_pubkey: offline_pubkey, stake_authorize: StakeAuthorize::Staker, - authority: None, + authority: 0, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::All, nonce_account: None, - nonce_authority: None, - fee_payer: Some(read_keypair_file(&payer_keypair_file).unwrap().into()), + nonce_authority: 0, + fee_payer: 1, }; process_command(&config).unwrap(); // `config` balance has not changed, despite submitting the TX - check_balance( - 50_000 - SIG_FEE - SIG_FEE, - &rpc_client, - &config.keypair.pubkey(), - ); + check_balance(50_000 - SIG_FEE - SIG_FEE, &rpc_client, &default_pubkey); // `config_payer` however has paid `config`'s authority sig // and `config_payer`'s fee sig - check_balance( - 100_000 - SIG_FEE - SIG_FEE, - &rpc_client, - &config_payer.keypair.pubkey(), - ); + check_balance(100_000 - SIG_FEE - SIG_FEE, &rpc_client, &payer_pubkey); // Assign authority with offline fee payer let (blockhash, _) = rpc_client.get_recent_blockhash().unwrap(); @@ -853,42 +830,34 @@ fn test_stake_authorize_with_fee_payer() { stake_account_pubkey, new_authorized_pubkey: payer_pubkey, stake_authorize: StakeAuthorize::Staker, - authority: None, + authority: 0, sign_only: true, - signers: None, blockhash_query: BlockhashQuery::None(blockhash, FeeCalculator::default()), nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }; let sign_reply = process_command(&config_offline).unwrap(); let (blockhash, signers) = parse_sign_only_reply_string(&sign_reply); + let offline_presigner = presigner_from_pubkey_sigs(&offline_pubkey, &signers).unwrap(); + config.signers = vec![&offline_presigner]; config.command = CliCommand::StakeAuthorize { stake_account_pubkey, new_authorized_pubkey: payer_pubkey, stake_authorize: StakeAuthorize::Staker, - authority: Some(offline_pubkey.into()), + authority: 0, sign_only: false, - signers: Some(signers), blockhash_query: BlockhashQuery::FeeCalculator(blockhash), nonce_account: None, - nonce_authority: None, - fee_payer: Some(offline_pubkey.into()), + nonce_authority: 0, + fee_payer: 0, }; process_command(&config).unwrap(); // `config`'s balance again has not changed - check_balance( - 50_000 - SIG_FEE - SIG_FEE, - &rpc_client, - &config.keypair.pubkey(), - ); + check_balance(50_000 - SIG_FEE - SIG_FEE, &rpc_client, &default_pubkey); // `config_offline` however has paid 1 sig due to being both authority // and fee payer - check_balance( - 100_000 - SIG_FEE, - &rpc_client, - &config_offline.keypair.pubkey(), - ); + check_balance(100_000 - SIG_FEE, &rpc_client, &offline_pubkey); server.close().unwrap(); remove_dir_all(ledger_path).unwrap(); @@ -904,22 +873,29 @@ fn test_stake_split() { let faucet_addr = receiver.recv().unwrap(); let rpc_client = RpcClient::new_socket(leader_data.rpc); + let default_signer = Keypair::new(); + let offline_signer = Keypair::new(); let mut config = CliConfig::default(); config.json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); + config.signers = vec![&default_signer]; let mut config_offline = CliConfig::default(); config_offline.json_rpc_url = String::default(); - let offline_pubkey = config_offline.keypair.pubkey(); - let (_offline_keypair_file, mut tmp_file) = make_tmp_file(); - write_keypair(&config_offline.keypair, tmp_file.as_file_mut()).unwrap(); + config_offline.signers = vec![&offline_signer]; + let offline_pubkey = config_offline.signers[0].pubkey(); // Verify we're offline config_offline.command = CliCommand::ClusterVersion; process_command(&config_offline).unwrap_err(); - request_and_confirm_airdrop(&rpc_client, &faucet_addr, &config.keypair.pubkey(), 500_000) - .unwrap(); - check_balance(500_000, &rpc_client, &config.keypair.pubkey()); + request_and_confirm_airdrop( + &rpc_client, + &faucet_addr, + &config.signers[0].pubkey(), + 500_000, + ) + .unwrap(); + check_balance(500_000, &rpc_client, &config.signers[0].pubkey()); request_and_confirm_airdrop(&rpc_client, &faucet_addr, &offline_pubkey, 100_000).unwrap(); check_balance(100_000, &rpc_client, &offline_pubkey); @@ -930,22 +906,20 @@ fn test_stake_split() { .unwrap(); let stake_keypair = keypair_from_seed(&[0u8; 32]).unwrap(); let stake_account_pubkey = stake_keypair.pubkey(); - let (stake_keypair_file, mut tmp_file) = make_tmp_file(); - write_keypair(&stake_keypair, tmp_file.as_file_mut()).unwrap(); + config.signers.push(&stake_keypair); config.command = CliCommand::CreateStakeAccount { - stake_account: read_keypair_file(&stake_keypair_file).unwrap().into(), + stake_account: 1, seed: None, staker: Some(offline_pubkey), withdrawer: Some(offline_pubkey), lockup: Lockup::default(), lamports: 10 * minimum_stake_balance, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::All, nonce_account: None, - nonce_authority: None, - fee_payer: None, - from: None, + nonce_authority: 0, + fee_payer: 0, + from: 0, }; process_command(&config).unwrap(); check_balance( @@ -959,20 +933,18 @@ fn test_stake_split() { .get_minimum_balance_for_rent_exemption(NonceState::size()) .unwrap(); let nonce_account = keypair_from_seed(&[1u8; 32]).unwrap(); - let nonce_account_pubkey = nonce_account.pubkey(); - let (nonce_keypair_file, mut tmp_file) = make_tmp_file(); - write_keypair(&nonce_account, tmp_file.as_file_mut()).unwrap(); + config.signers = vec![&default_signer, &nonce_account]; config.command = CliCommand::CreateNonceAccount { - nonce_account: read_keypair_file(&nonce_keypair_file).unwrap().into(), + nonce_account: 1, seed: None, nonce_authority: Some(offline_pubkey), lamports: minimum_nonce_balance, }; process_command(&config).unwrap(); - check_balance(minimum_nonce_balance, &rpc_client, &nonce_account_pubkey); + check_balance(minimum_nonce_balance, &rpc_client, &nonce_account.pubkey()); // Fetch nonce hash - let account = rpc_client.get_account(&nonce_account_pubkey).unwrap(); + let account = rpc_client.get_account(&nonce_account.pubkey()).unwrap(); let nonce_state: NonceState = account.state().unwrap(); let nonce_hash = match nonce_state { NonceState::Initialized(_meta, hash) => hash, @@ -981,36 +953,35 @@ fn test_stake_split() { // Nonced offline split let split_account = keypair_from_seed(&[2u8; 32]).unwrap(); - let (split_keypair_file, mut tmp_file) = make_tmp_file(); - write_keypair(&split_account, tmp_file.as_file_mut()).unwrap(); check_balance(0, &rpc_client, &split_account.pubkey()); + config_offline.signers.push(&split_account); config_offline.command = CliCommand::SplitStake { stake_account_pubkey: stake_account_pubkey, - stake_authority: None, + stake_authority: 0, sign_only: true, - signers: None, blockhash_query: BlockhashQuery::None(nonce_hash, FeeCalculator::default()), - nonce_account: Some(nonce_account_pubkey), - nonce_authority: None, - split_stake_account: read_keypair_file(&split_keypair_file).unwrap().into(), + nonce_account: Some(nonce_account.pubkey()), + nonce_authority: 0, + split_stake_account: 1, seed: None, lamports: 2 * minimum_stake_balance, - fee_payer: None, + fee_payer: 0, }; let sig_response = process_command(&config_offline).unwrap(); let (blockhash, signers) = parse_sign_only_reply_string(&sig_response); + let offline_presigner = presigner_from_pubkey_sigs(&offline_pubkey, &signers).unwrap(); + config.signers = vec![&offline_presigner, &split_account]; config.command = CliCommand::SplitStake { stake_account_pubkey: stake_account_pubkey, - stake_authority: Some(offline_pubkey.into()), + stake_authority: 0, sign_only: false, - signers: Some(signers), blockhash_query: BlockhashQuery::FeeCalculator(blockhash), - nonce_account: Some(nonce_account_pubkey), - nonce_authority: Some(offline_pubkey.into()), - split_stake_account: read_keypair_file(&split_keypair_file).unwrap().into(), + nonce_account: Some(nonce_account.pubkey()), + nonce_authority: 0, + split_stake_account: 1, seed: None, lamports: 2 * minimum_stake_balance, - fee_payer: Some(offline_pubkey.into()), + fee_payer: 0, }; process_command(&config).unwrap(); check_balance( @@ -1038,22 +1009,29 @@ fn test_stake_set_lockup() { let faucet_addr = receiver.recv().unwrap(); let rpc_client = RpcClient::new_socket(leader_data.rpc); + let default_signer = Keypair::new(); + let offline_signer = Keypair::new(); let mut config = CliConfig::default(); config.json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); + config.signers = vec![&default_signer]; let mut config_offline = CliConfig::default(); config_offline.json_rpc_url = String::default(); - let offline_pubkey = config_offline.keypair.pubkey(); - let (_offline_keypair_file, mut tmp_file) = make_tmp_file(); - write_keypair(&config_offline.keypair, tmp_file.as_file_mut()).unwrap(); + config_offline.signers = vec![&offline_signer]; + let offline_pubkey = config_offline.signers[0].pubkey(); // Verify we're offline config_offline.command = CliCommand::ClusterVersion; process_command(&config_offline).unwrap_err(); - request_and_confirm_airdrop(&rpc_client, &faucet_addr, &config.keypair.pubkey(), 500_000) - .unwrap(); - check_balance(500_000, &rpc_client, &config.keypair.pubkey()); + request_and_confirm_airdrop( + &rpc_client, + &faucet_addr, + &config.signers[0].pubkey(), + 500_000, + ) + .unwrap(); + check_balance(500_000, &rpc_client, &config.signers[0].pubkey()); request_and_confirm_airdrop(&rpc_client, &faucet_addr, &offline_pubkey, 100_000).unwrap(); check_balance(100_000, &rpc_client, &offline_pubkey); @@ -1065,26 +1043,24 @@ fn test_stake_set_lockup() { let stake_keypair = keypair_from_seed(&[0u8; 32]).unwrap(); let stake_account_pubkey = stake_keypair.pubkey(); - let (stake_keypair_file, mut tmp_file) = make_tmp_file(); - write_keypair(&stake_keypair, tmp_file.as_file_mut()).unwrap(); let mut lockup = Lockup::default(); - lockup.custodian = config.keypair.pubkey(); + lockup.custodian = config.signers[0].pubkey(); + config.signers.push(&stake_keypair); config.command = CliCommand::CreateStakeAccount { - stake_account: read_keypair_file(&stake_keypair_file).unwrap().into(), + stake_account: 1, seed: None, staker: Some(offline_pubkey), withdrawer: Some(offline_pubkey), lockup, lamports: 10 * minimum_stake_balance, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::All, nonce_account: None, - nonce_authority: None, - fee_payer: None, - from: None, + nonce_authority: 0, + fee_payer: 0, + from: 0, }; process_command(&config).unwrap(); check_balance( @@ -1099,16 +1075,16 @@ fn test_stake_set_lockup() { epoch: 200, custodian: Pubkey::default(), }; + config.signers.pop(); config.command = CliCommand::StakeSetLockup { stake_account_pubkey, lockup, - custodian: None, + custodian: 0, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::default(), nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }; process_command(&config).unwrap(); let stake_account = rpc_client.get_account(&stake_account_pubkey).unwrap(); @@ -1117,14 +1093,12 @@ fn test_stake_set_lockup() { StakeState::Initialized(meta) => meta.lockup, _ => panic!("Unexpected stake state!"), }; - lockup.custodian = config.keypair.pubkey(); // Default new_custodian is config.keypair + lockup.custodian = config.signers[0].pubkey(); // Default new_custodian is config.signers[0] assert_eq!(current_lockup, lockup); // Set custodian to another pubkey let online_custodian = Keypair::new(); let online_custodian_pubkey = online_custodian.pubkey(); - let (online_custodian_file, mut tmp_file) = make_tmp_file(); - write_keypair(&online_custodian, tmp_file.as_file_mut()).unwrap(); let lockup = Lockup { unix_timestamp: 1581534571, @@ -1134,13 +1108,12 @@ fn test_stake_set_lockup() { config.command = CliCommand::StakeSetLockup { stake_account_pubkey, lockup, - custodian: None, + custodian: 0, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::default(), nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }; process_command(&config).unwrap(); @@ -1149,16 +1122,16 @@ fn test_stake_set_lockup() { epoch: 202, custodian: Pubkey::default(), }; + config.signers = vec![&default_signer, &online_custodian]; config.command = CliCommand::StakeSetLockup { stake_account_pubkey, lockup, - custodian: Some(read_keypair_file(&online_custodian_file).unwrap().into()), + custodian: 1, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::default(), nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }; process_command(&config).unwrap(); let stake_account = rpc_client.get_account(&stake_account_pubkey).unwrap(); @@ -1179,13 +1152,12 @@ fn test_stake_set_lockup() { config.command = CliCommand::StakeSetLockup { stake_account_pubkey, lockup, - custodian: Some(online_custodian.into()), + custodian: 1, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::default(), nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }; process_command(&config).unwrap(); @@ -1195,10 +1167,9 @@ fn test_stake_set_lockup() { .unwrap(); let nonce_account = keypair_from_seed(&[1u8; 32]).unwrap(); let nonce_account_pubkey = nonce_account.pubkey(); - let (nonce_keypair_file, mut tmp_file) = make_tmp_file(); - write_keypair(&nonce_account, tmp_file.as_file_mut()).unwrap(); + config.signers = vec![&default_signer, &nonce_account]; config.command = CliCommand::CreateNonceAccount { - nonce_account: read_keypair_file(&nonce_keypair_file).unwrap().into(), + nonce_account: 1, seed: None, nonce_authority: Some(offline_pubkey), lamports: minimum_nonce_balance, @@ -1223,26 +1194,26 @@ fn test_stake_set_lockup() { config_offline.command = CliCommand::StakeSetLockup { stake_account_pubkey, lockup, - custodian: None, + custodian: 0, sign_only: true, - signers: None, blockhash_query: BlockhashQuery::None(nonce_hash, FeeCalculator::default()), nonce_account: Some(nonce_account_pubkey), - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }; let sig_response = process_command(&config_offline).unwrap(); let (blockhash, signers) = parse_sign_only_reply_string(&sig_response); + let offline_presigner = presigner_from_pubkey_sigs(&offline_pubkey, &signers).unwrap(); + config.signers = vec![&offline_presigner]; config.command = CliCommand::StakeSetLockup { stake_account_pubkey, lockup, - custodian: Some(offline_pubkey.into()), + custodian: 0, sign_only: false, - signers: Some(signers), blockhash_query: BlockhashQuery::FeeCalculator(blockhash), nonce_account: Some(nonce_account_pubkey), - nonce_authority: Some(offline_pubkey.into()), - fee_payer: Some(offline_pubkey.into()), + nonce_authority: 0, + fee_payer: 0, }; process_command(&config).unwrap(); let stake_account = rpc_client.get_account(&stake_account_pubkey).unwrap(); @@ -1269,31 +1240,30 @@ fn test_offline_nonced_create_stake_account_and_withdraw() { let rpc_client = RpcClient::new_socket(leader_data.rpc); let mut config = CliConfig::default(); - config.keypair = keypair_from_seed(&[1u8; 32]).unwrap(); + let default_signer = keypair_from_seed(&[1u8; 32]).unwrap(); + config.signers = vec![&default_signer]; config.json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); let mut config_offline = CliConfig::default(); - config_offline.keypair = keypair_from_seed(&[2u8; 32]).unwrap(); - let offline_pubkey = config_offline.keypair.pubkey(); - let (offline_keypair_file, mut tmp_file) = make_tmp_file(); - write_keypair(&config_offline.keypair, tmp_file.as_file_mut()).unwrap(); + let offline_signer = keypair_from_seed(&[2u8; 32]).unwrap(); + config_offline.signers = vec![&offline_signer]; + let offline_pubkey = config_offline.signers[0].pubkey(); config_offline.json_rpc_url = String::default(); config_offline.command = CliCommand::ClusterVersion; // Verfiy that we cannot reach the cluster process_command(&config_offline).unwrap_err(); - request_and_confirm_airdrop(&rpc_client, &faucet_addr, &config.keypair.pubkey(), 200_000) - .unwrap(); - check_balance(200_000, &rpc_client, &config.keypair.pubkey()); - request_and_confirm_airdrop( &rpc_client, &faucet_addr, - &config_offline.keypair.pubkey(), - 100_000, + &config.signers[0].pubkey(), + 200_000, ) .unwrap(); - check_balance(100_000, &rpc_client, &config_offline.keypair.pubkey()); + check_balance(200_000, &rpc_client, &config.signers[0].pubkey()); + + request_and_confirm_airdrop(&rpc_client, &faucet_addr, &offline_pubkey, 100_000).unwrap(); + check_balance(100_000, &rpc_client, &offline_pubkey); // Create nonce account let minimum_nonce_balance = rpc_client @@ -1301,10 +1271,9 @@ fn test_offline_nonced_create_stake_account_and_withdraw() { .unwrap(); let nonce_account = keypair_from_seed(&[3u8; 32]).unwrap(); let nonce_pubkey = nonce_account.pubkey(); - let (nonce_keypair_file, mut tmp_file) = make_tmp_file(); - write_keypair(&nonce_account, tmp_file.as_file_mut()).unwrap(); + config.signers.push(&nonce_account); config.command = CliCommand::CreateNonceAccount { - nonce_account: read_keypair_file(&nonce_keypair_file).unwrap().into(), + nonce_account: 1, seed: None, nonce_authority: Some(offline_pubkey), lamports: minimum_nonce_balance, @@ -1322,39 +1291,39 @@ fn test_offline_nonced_create_stake_account_and_withdraw() { // Create stake account offline let stake_keypair = keypair_from_seed(&[4u8; 32]).unwrap(); let stake_pubkey = stake_keypair.pubkey(); - let (stake_keypair_file, mut tmp_file) = make_tmp_file(); - write_keypair(&stake_keypair, tmp_file.as_file_mut()).unwrap(); + config_offline.signers.push(&stake_keypair); config_offline.command = CliCommand::CreateStakeAccount { - stake_account: read_keypair_file(&stake_keypair_file).unwrap().into(), + stake_account: 1, seed: None, staker: None, withdrawer: None, lockup: Lockup::default(), lamports: 50_000, sign_only: true, - signers: None, blockhash_query: BlockhashQuery::None(nonce_hash, FeeCalculator::default()), nonce_account: Some(nonce_pubkey), - nonce_authority: None, - fee_payer: None, - from: None, + nonce_authority: 0, + fee_payer: 0, + from: 0, }; let sig_response = process_command(&config_offline).unwrap(); let (blockhash, signers) = parse_sign_only_reply_string(&sig_response); + let offline_presigner = presigner_from_pubkey_sigs(&offline_pubkey, &signers).unwrap(); + let stake_presigner = presigner_from_pubkey_sigs(&stake_pubkey, &signers).unwrap(); + config.signers = vec![&offline_presigner, &stake_presigner]; config.command = CliCommand::CreateStakeAccount { - stake_account: stake_pubkey.into(), + stake_account: 1, seed: None, - staker: Some(offline_pubkey.into()), + staker: Some(offline_pubkey), withdrawer: None, lockup: Lockup::default(), lamports: 50_000, sign_only: false, - signers: Some(signers), blockhash_query: BlockhashQuery::FeeCalculator(blockhash), nonce_account: Some(nonce_pubkey), - nonce_authority: Some(offline_pubkey.into()), - fee_payer: Some(offline_pubkey.into()), - from: Some(offline_pubkey.into()), + nonce_authority: 0, + fee_payer: 0, + from: 0, }; process_command(&config).unwrap(); check_balance(50_000, &rpc_client, &stake_pubkey); @@ -1370,52 +1339,84 @@ fn test_offline_nonced_create_stake_account_and_withdraw() { // Offline, nonced stake-withdraw let recipient = keypair_from_seed(&[5u8; 32]).unwrap(); let recipient_pubkey = recipient.pubkey(); + config_offline.signers.pop(); config_offline.command = CliCommand::WithdrawStake { stake_account_pubkey: stake_pubkey, destination_account_pubkey: recipient_pubkey, lamports: 42, - withdraw_authority: None, + withdraw_authority: 0, sign_only: true, - signers: None, blockhash_query: BlockhashQuery::None(nonce_hash, FeeCalculator::default()), nonce_account: Some(nonce_pubkey), - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }; let sig_response = process_command(&config_offline).unwrap(); let (blockhash, signers) = parse_sign_only_reply_string(&sig_response); + let offline_presigner = presigner_from_pubkey_sigs(&offline_pubkey, &signers).unwrap(); + config.signers = vec![&offline_presigner]; config.command = CliCommand::WithdrawStake { stake_account_pubkey: stake_pubkey, destination_account_pubkey: recipient_pubkey, lamports: 42, - withdraw_authority: Some(offline_pubkey.into()), + withdraw_authority: 0, sign_only: false, - signers: Some(signers), blockhash_query: BlockhashQuery::FeeCalculator(blockhash), nonce_account: Some(nonce_pubkey), - nonce_authority: Some(offline_pubkey.into()), - fee_payer: Some(offline_pubkey.into()), + nonce_authority: 0, + fee_payer: 0, }; process_command(&config).unwrap(); check_balance(42, &rpc_client, &recipient_pubkey); - // Test that offline derived addresses fail + // Fetch nonce hash + let account = rpc_client.get_account(&nonce_account.pubkey()).unwrap(); + let nonce_state: NonceState = account.state().unwrap(); + let nonce_hash = match nonce_state { + NonceState::Initialized(_meta, hash) => hash, + _ => panic!("Nonce is not initialized"), + }; + + // Create another stake account. This time with seed + let seed = "seedy"; + config_offline.signers = vec![&offline_signer, &stake_keypair]; config_offline.command = CliCommand::CreateStakeAccount { - stake_account: read_keypair_file(&stake_keypair_file).unwrap().into(), - seed: Some("fail".to_string()), + stake_account: 1, + seed: Some(seed.to_string()), staker: None, withdrawer: None, lockup: Lockup::default(), lamports: 50_000, sign_only: true, - signers: None, blockhash_query: BlockhashQuery::None(nonce_hash, FeeCalculator::default()), nonce_account: Some(nonce_pubkey), - nonce_authority: Some(read_keypair_file(&offline_keypair_file).unwrap().into()), - fee_payer: Some(read_keypair_file(&offline_keypair_file).unwrap().into()), - from: Some(read_keypair_file(&offline_keypair_file).unwrap().into()), + nonce_authority: 0, + fee_payer: 0, + from: 0, }; - process_command(&config_offline).unwrap_err(); + let sig_response = process_command(&config_offline).unwrap(); + let (blockhash, signers) = parse_sign_only_reply_string(&sig_response); + let offline_presigner = presigner_from_pubkey_sigs(&offline_pubkey, &signers).unwrap(); + let stake_presigner = presigner_from_pubkey_sigs(&stake_pubkey, &signers).unwrap(); + config.signers = vec![&offline_presigner, &stake_presigner]; + config.command = CliCommand::CreateStakeAccount { + stake_account: 1, + seed: Some(seed.to_string()), + staker: Some(offline_pubkey.into()), + withdrawer: Some(offline_pubkey.into()), + lockup: Lockup::default(), + lamports: 50_000, + sign_only: false, + blockhash_query: BlockhashQuery::FeeCalculator(blockhash), + nonce_account: Some(nonce_pubkey), + nonce_authority: 0, + fee_payer: 0, + from: 0, + }; + process_command(&config).unwrap(); + let seed_address = + create_address_with_seed(&stake_pubkey, seed, &solana_stake_program::id()).unwrap(); + check_balance(50_000, &rpc_client, &seed_address); server.close().unwrap(); remove_dir_all(ledger_path).unwrap(); diff --git a/cli/tests/transfer.rs b/cli/tests/transfer.rs index 487ae679cc..a276128f1a 100644 --- a/cli/tests/transfer.rs +++ b/cli/tests/transfer.rs @@ -1,3 +1,4 @@ +use solana_clap_utils::keypair::presigner_from_pubkey_sigs; use solana_cli::{ cli::{process_command, request_and_confirm_airdrop, CliCommand, CliConfig}, offline::{parse_sign_only_reply_string, BlockhashQuery}, @@ -9,21 +10,12 @@ use solana_sdk::{ fee_calculator::FeeCalculator, nonce_state::NonceState, 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::sync::mpsc::channel; +use std::{fs::remove_dir_all, sync::mpsc::channel, thread::sleep, time::Duration}; #[cfg(test)] 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) { (0..5).for_each(|tries| { @@ -48,13 +40,15 @@ fn test_transfer() { 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(); 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]); - println!("sender: {:?}", sender_pubkey); - println!("recipient: {:?}", recipient_pubkey); request_and_confirm_airdrop(&rpc_client, &faucet_addr, &sender_pubkey, 50_000).unwrap(); check_balance(50_000, &rpc_client, &sender_pubkey); @@ -64,13 +58,12 @@ fn test_transfer() { config.command = CliCommand::Transfer { lamports: 10, to: recipient_pubkey, - from: None, + from: 0, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::All, nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }; process_command(&config).unwrap(); check_balance(49_989, &rpc_client, &sender_pubkey); @@ -78,12 +71,12 @@ fn test_transfer() { let mut offline = CliConfig::default(); offline.json_rpc_url = String::default(); + offline.signers = vec![&default_offline_signer]; // Verify we cannot contact the cluster offline.command = CliCommand::ClusterVersion; process_command(&offline).unwrap_err(); - let offline_pubkey = offline.keypair.pubkey(); - println!("offline: {:?}", offline_pubkey); + let offline_pubkey = offline.signers[0].pubkey(); request_and_confirm_airdrop(&rpc_client, &faucet_addr, &offline_pubkey, 50).unwrap(); check_balance(50, &rpc_client, &offline_pubkey); @@ -92,26 +85,26 @@ fn test_transfer() { offline.command = CliCommand::Transfer { lamports: 10, to: recipient_pubkey, - from: None, + from: 0, sign_only: true, - signers: None, blockhash_query: BlockhashQuery::None(blockhash, FeeCalculator::default()), nonce_account: None, - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }; let sign_only_reply = process_command(&offline).unwrap(); 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 { lamports: 10, to: recipient_pubkey, - from: Some(offline_pubkey.into()), + from: 0, sign_only: false, - signers: Some(signers), blockhash_query: BlockhashQuery::FeeCalculator(blockhash), nonce_account: None, - nonce_authority: None, - fee_payer: Some(offline_pubkey.into()), + nonce_authority: 0, + fee_payer: 0, }; process_command(&config).unwrap(); check_balance(39, &rpc_client, &offline_pubkey); @@ -119,13 +112,12 @@ fn test_transfer() { // Create nonce account 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 .get_minimum_balance_for_rent_exemption(NonceState::size()) .unwrap(); + config.signers = vec![&default_signer, &nonce_account]; config.command = CliCommand::CreateNonceAccount { - nonce_account: read_keypair_file(&nonce_account_file).unwrap().into(), + nonce_account: 1, seed: None, nonce_authority: None, lamports: minimum_nonce_balance, @@ -142,16 +134,16 @@ fn test_transfer() { }; // Nonced transfer + config.signers = vec![&default_signer]; config.command = CliCommand::Transfer { lamports: 10, to: recipient_pubkey, - from: None, + from: 0, sign_only: false, - signers: None, blockhash_query: BlockhashQuery::FeeCalculator(nonce_hash), nonce_account: Some(nonce_account.pubkey()), - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }; process_command(&config).unwrap(); 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); // Assign nonce authority to offline + config.signers = vec![&default_signer]; config.command = CliCommand::AuthorizeNonceAccount { nonce_account: nonce_account.pubkey(), - nonce_authority: None, + nonce_authority: 0, new_authority: offline_pubkey, }; process_command(&config).unwrap(); @@ -182,29 +175,30 @@ fn test_transfer() { }; // Offline, nonced transfer + offline.signers = vec![&default_offline_signer]; offline.command = CliCommand::Transfer { lamports: 10, to: recipient_pubkey, - from: None, + from: 0, sign_only: true, - signers: None, blockhash_query: BlockhashQuery::None(nonce_hash, FeeCalculator::default()), nonce_account: Some(nonce_account.pubkey()), - nonce_authority: None, - fee_payer: None, + nonce_authority: 0, + fee_payer: 0, }; let sign_only_reply = process_command(&offline).unwrap(); 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 { lamports: 10, to: recipient_pubkey, - from: Some(offline_pubkey.into()), + from: 0, sign_only: false, - signers: Some(signers), blockhash_query: BlockhashQuery::FeeCalculator(blockhash), nonce_account: Some(nonce_account.pubkey()), - nonce_authority: Some(offline_pubkey.into()), - fee_payer: Some(offline_pubkey.into()), + nonce_authority: 0, + fee_payer: 0, }; process_command(&config).unwrap(); check_balance(28, &rpc_client, &offline_pubkey); diff --git a/keygen/src/keygen.rs b/keygen/src/keygen.rs index cd9c1ccb3a..cbf071138a 100644 --- a/keygen/src/keygen.rs +++ b/keygen/src/keygen.rs @@ -6,21 +6,17 @@ use clap::{ }; use num_cpus; use solana_clap_utils::{ - input_parsers::derivation_of, input_validators::is_derivation, 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, }, }; 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::{ pubkey::write_pubkey_file, - signature::{ - keypair_from_seed, read_keypair, read_keypair_file, write_keypair, write_keypair_file, - Keypair, Signer, - }, + signature::{keypair_from_seed, write_keypair, write_keypair_file, Keypair, Signer}, }; use std::{ collections::HashSet, @@ -54,6 +50,7 @@ fn check_for_overwrite(outfile: &str, matches: &ArgMatches) { fn get_keypair_from_matches( matches: &ArgMatches, config: Config, + wallet_manager: Option>, ) -> Result, Box> { let mut path = dirs::home_dir().expect("home directory"); let path = if matches.is_present("keypair") { @@ -64,26 +61,7 @@ fn get_keypair_from_matches( path.extend(&[".config", "solana", "id.json"]); path.to_str().unwrap() }; - - 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"), - )?)), - } + signer_from_path(matches, path, "pubkey recovery", wallet_manager.as_ref()) } fn output_keypair( @@ -404,9 +382,11 @@ fn main() -> Result<(), Box> { Config::default() }; + let wallet_manager = maybe_wallet_manager()?; + match matches.subcommand() { ("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") { let outfile = matches.value_of("outfile").unwrap(); @@ -583,7 +563,7 @@ fn main() -> Result<(), Box> { } } ("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 signature = keypair.try_sign_message(test_data)?; let pubkey_bs58 = matches.value_of("pubkey").unwrap(); diff --git a/remote-wallet/Cargo.toml b/remote-wallet/Cargo.toml index f26dc15243..5716fbbc25 100644 --- a/remote-wallet/Cargo.toml +++ b/remote-wallet/Cargo.toml @@ -17,6 +17,7 @@ parking_lot = "0.7" semver = "0.9" solana-sdk = { path = "../sdk", version = "0.23.7" } thiserror = "1.0" +url = "2.1.1" [features] default = ["linux-static-hidraw"] diff --git a/remote-wallet/src/ledger.rs b/remote-wallet/src/ledger.rs index b4c403e012..671a6daf27 100644 --- a/remote-wallet/src/ledger.rs +++ b/remote-wallet/src/ledger.rs @@ -1,5 +1,5 @@ use crate::remote_wallet::{ - initialize_wallet_manager, DerivationPath, RemoteWallet, RemoteWalletError, RemoteWalletInfo, + DerivationPath, RemoteWallet, RemoteWalletError, RemoteWalletInfo, RemoteWalletManager, }; use dialoguer::{theme::ColorfulTheme, Select}; use log::*; @@ -7,6 +7,8 @@ use semver::Version as FirmwareVersion; use solana_sdk::{pubkey::Pubkey, signature::Signature}; use std::{cmp::min, fmt, sync::Arc}; +const HARDENED_BIT: u32 = 1 << 31; + const APDU_TAG: u8 = 0x05; const APDU_CLA: u8 = 0xe0; const APDU_PAYLOAD_HEADER_LEN: usize = 8; @@ -41,7 +43,7 @@ const HID_PREFIX_ZERO: usize = 0; mod commands { #[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 SIGN_MESSAGE: u8 = 0x03; } @@ -74,7 +76,7 @@ impl LedgerWallet { // * APDU_INS (1 byte) // * APDU_P1 (1 byte) // * APDU_P2 (1 byte) - // * APDU_LENGTH (1 byte) + // * APDU_LENGTH (2 bytes) // * APDU_Payload (Variable) // fn write(&self, command: u8, p1: u8, p2: u8, data: &[u8]) -> Result<(), RemoteWalletError> { @@ -139,11 +141,6 @@ impl LedgerWallet { // * Payload (Optional) // // 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_Payload (Variable) // @@ -193,6 +190,10 @@ impl LedgerWallet { match status { // These need to be aligned with solana Ledger app error codes, and clippy allowance removed 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( "Security status not satisfied (Canceled by user)", )), @@ -260,7 +261,7 @@ impl RemoteWallet for LedgerWallet { .serial_number .clone() .unwrap_or_else(|| "Unknown".to_owned()); - self.get_pubkey(&DerivationPath::default()) + self.get_pubkey(&DerivationPath::default(), false) .map(|pubkey| RemoteWalletInfo { model, manufacturer, @@ -269,12 +270,16 @@ impl RemoteWallet for LedgerWallet { }) } - fn get_pubkey(&self, derivation_path: &DerivationPath) -> Result { + fn get_pubkey( + &self, + derivation_path: &DerivationPath, + confirm_key: bool, + ) -> Result { let derivation_path = extend_and_serialize(derivation_path); let key = self.send_apdu( commands::GET_PUBKEY, - 0, // In the naive implementation, default request is for no device confirmation + if confirm_key { 1 } else { 0 }, 0, &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 { let byte = if derivation_path.change.is_some() { 4 - } else { + } else if derivation_path.account.is_some() { 3 + } else { + 2 }; let mut concat_derivation = vec![byte]; concat_derivation.extend_from_slice(&SOL_DERIVATION_PATH_BE); - concat_derivation.extend_from_slice(&[0x80, 0]); - concat_derivation.extend_from_slice(&derivation_path.account.to_be_bytes()); - if let Some(change) = derivation_path.change { - concat_derivation.extend_from_slice(&[0x80, 0]); - concat_derivation.extend_from_slice(&change.to_be_bytes()); + if let Some(account) = derivation_path.account { + let hardened_account = account | HARDENED_BIT; + concat_derivation.extend_from_slice(&hardened_account.to_be_bytes()); + if let Some(change) = derivation_path.change { + let hardened_change = change | HARDENED_BIT; + concat_derivation.extend_from_slice(&hardened_change.to_be_bytes()); + } } concat_derivation } @@ -378,9 +387,8 @@ fn extend_and_serialize(derivation_path: &DerivationPath) -> Vec { /// Choose a Ledger wallet based on matching info fields pub fn get_ledger_from_info( info: RemoteWalletInfo, + wallet_manager: &RemoteWalletManager, ) -> Result, RemoteWalletError> { - let wallet_manager = initialize_wallet_manager(); - let _device_count = wallet_manager.update_devices()?; let devices = wallet_manager.list_devices(); let (pubkeys, device_paths): (Vec, Vec) = devices .iter() diff --git a/remote-wallet/src/remote_keypair.rs b/remote-wallet/src/remote_keypair.rs index 06ebaa1733..7cd656f3bd 100644 --- a/remote-wallet/src/remote_keypair.rs +++ b/remote-wallet/src/remote_keypair.rs @@ -1,7 +1,8 @@ use crate::{ ledger::get_ledger_from_info, remote_wallet::{ - DerivationPath, RemoteWallet, RemoteWalletError, RemoteWalletInfo, RemoteWalletType, + DerivationPath, RemoteWallet, RemoteWalletError, RemoteWalletInfo, RemoteWalletManager, + RemoteWalletType, }, }; use solana_sdk::{ @@ -12,24 +13,30 @@ use solana_sdk::{ pub struct RemoteKeypair { pub wallet_type: RemoteWalletType, pub derivation_path: DerivationPath, + pub pubkey: Pubkey, } impl RemoteKeypair { - pub fn new(wallet_type: RemoteWalletType, derivation_path: DerivationPath) -> Self { - Self { + pub fn new( + wallet_type: RemoteWalletType, + derivation_path: DerivationPath, + confirm_key: bool, + ) -> Result { + let pubkey = match &wallet_type { + RemoteWalletType::Ledger(wallet) => wallet.get_pubkey(&derivation_path, confirm_key)?, + }; + + Ok(Self { wallet_type, derivation_path, - } + pubkey, + }) } } impl Signer for RemoteKeypair { fn try_pubkey(&self) -> Result { - match &self.wallet_type { - RemoteWalletType::Ledger(wallet) => wallet - .get_pubkey(&self.derivation_path) - .map_err(|e| e.into()), - } + Ok(self.pubkey) } fn try_sign_message(&self, message: &[u8]) -> Result { @@ -44,17 +51,20 @@ impl Signer for RemoteKeypair { pub fn generate_remote_keypair( path: String, explicit_derivation_path: Option, + wallet_manager: &RemoteWalletManager, + confirm_key: bool, ) -> Result { let (remote_wallet_info, mut derivation_path) = RemoteWalletInfo::parse_path(path)?; if let Some(derivation) = explicit_derivation_path { derivation_path = derivation; } if remote_wallet_info.manufacturer == "ledger" { - let ledger = get_ledger_from_info(remote_wallet_info)?; - Ok(RemoteKeypair { - wallet_type: RemoteWalletType::Ledger(ledger), + let ledger = get_ledger_from_info(remote_wallet_info, wallet_manager)?; + Ok(RemoteKeypair::new( + RemoteWalletType::Ledger(ledger), derivation_path, - }) + confirm_key, + )?) } else { Err(RemoteWalletError::DeviceTypeMismatch) } diff --git a/remote-wallet/src/remote_wallet.rs b/remote-wallet/src/remote_wallet.rs index 277d8222f0..991555b4f5 100644 --- a/remote-wallet/src/remote_wallet.rs +++ b/remote-wallet/src/remote_wallet.rs @@ -12,6 +12,7 @@ use std::{ time::{Duration, Instant}, }; use thiserror::Error; +use url::Url; const HID_GLOBAL_USAGE_PAGE: u16 = 0xFF00; const HID_USB_DEVICE_CLASS: u8 = 0; @@ -173,7 +174,11 @@ pub trait RemoteWallet { ) -> Result; /// Get solana pubkey from a RemoteWallet - fn get_pubkey(&self, derivation_path: &DerivationPath) -> Result; + fn get_pubkey( + &self, + derivation_path: &DerivationPath, + confirm_key: bool, + ) -> Result; /// Sign transaction data with wallet managing pubkey at derivation path m/44'/501'/'/'. fn sign_message( @@ -211,45 +216,68 @@ pub struct RemoteWalletInfo { } impl RemoteWalletInfo { - pub fn parse_path(mut path: String) -> Result<(Self, DerivationPath), RemoteWalletError> { - if path.ends_with('/') { - path.pop(); + pub fn parse_path(path: String) -> Result<(Self, DerivationPath), RemoteWalletError> { + let wallet_path = Url::parse(&path).map_err(|e| { + 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 manufacturer = parts.next().unwrap(); - wallet_info.manufacturer = manufacturer.to_string(); - wallet_info.model = parts.next().unwrap_or("").to_string(); - wallet_info.pubkey = parts - .next() - .and_then(|pubkey_str| Pubkey::from_str(pubkey_str).ok()) - .unwrap_or_default(); + wallet_info.manufacturer = wallet_path.host_str().unwrap().to_string(); + + if let Some(wallet_id) = wallet_path.path_segments().map(|c| c.collect::>()) { + wallet_info.model = wallet_id[0].to_string(); + if wallet_id.len() > 1 { + wallet_info.pubkey = Pubkey::from_str(wallet_id[1]).map_err(|e| { + RemoteWalletError::InvalidDerivationPath(format!( + "pubkey from_str error: {:?}", + e + )) + })?; + } + } let mut derivation_path = DerivationPath::default(); - if let Some(purpose) = parts.next() { - if purpose.replace("'", "") != "44" { - return Err(RemoteWalletError::InvalidDerivationPath(format!( - "Incorrect purpose number, found: {}, must be 44", - purpose - ))); - } - if let Some(coin) = parts.next() { - if coin.replace("'", "") != "501" { - return Err(RemoteWalletError::InvalidDerivationPath(format!( - "Incorrect coin number, found: {}, must be 501", - coin - ))); + let mut query_pairs = wallet_path.query_pairs(); + if query_pairs.count() > 0 { + for _ in 0..query_pairs.count() { + if let Some(mut pair) = query_pairs.next() { + 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(); + } + let mut parts = key_path.split('/'); + derivation_path.account = parts + .next() + .and_then(|account| account.replace("'", "").parse::().ok()); + derivation_path.change = parts + .next() + .and_then(|change| change.replace("'", "").parse::().ok()); + if parts.next().is_some() { + return Err(RemoteWalletError::InvalidDerivationPath(format!( + "key path `{}` too deep, only / supported", + _key_path + ))); + } + } else { + return Err(RemoteWalletError::InvalidDerivationPath(format!( + "invalid query string `{}={}`, only `key` supported", + pair.0, pair.1 + ))); + } } - if let Some(account) = parts.next() { - derivation_path.account = account.replace("'", "").parse::().unwrap(); - derivation_path.change = parts - .next() - .and_then(|change| change.replace("'", "").parse::().ok()); + if query_pairs.next().is_some() { + return Err(RemoteWalletError::InvalidDerivationPath( + "invalid query string, extra fields not supported".to_string(), + )); } - } else { - return Err(RemoteWalletError::InvalidDerivationPath( - "Derivation path too short, missing coin number".to_string(), - )); } } Ok((wallet_info, derivation_path)) @@ -273,18 +301,23 @@ impl RemoteWalletInfo { #[derive(Default, PartialEq, Clone)] pub struct DerivationPath { - pub account: u16, - pub change: Option, + pub account: Option, + pub change: Option, } impl fmt::Debug for DerivationPath { 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 { format!("/{:?}'", change) } else { "".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 -pub fn initialize_wallet_manager() -> Arc { - let hidapi = Arc::new(Mutex::new(hidapi::HidApi::new().unwrap())); - RemoteWalletManager::new(hidapi) +pub fn initialize_wallet_manager() -> Result, RemoteWalletError> { + let hidapi = Arc::new(Mutex::new(hidapi::HidApi::new()?)); + Ok(RemoteWalletManager::new(hidapi)) +} + +pub fn maybe_wallet_manager() -> Result>, 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)] @@ -307,22 +351,7 @@ mod tests { fn test_parse_path() { let pubkey = Pubkey::new_rand(); let (wallet_info, derivation_path) = - RemoteWalletInfo::parse_path(format!("ledger/nano-s/{:?}/44/501/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: 1, - change: Some(2), - } - ); - let (wallet_info, derivation_path) = - RemoteWalletInfo::parse_path(format!("ledger/nano-s/{:?}/44'/501'/1'/2'", pubkey)) + RemoteWalletInfo::parse_path(format!("usb://ledger/nano-s/{:?}?key=1/2", pubkey)) .unwrap(); assert!(wallet_info.matches(&RemoteWalletInfo { model: "nano-s".to_string(), @@ -333,17 +362,122 @@ mod tests { assert_eq!( derivation_path, 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), } ); + // Failure cases 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!( - 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] diff --git a/sdk/src/signature.rs b/sdk/src/signature.rs index 4acf63612a..50b9905d96 100644 --- a/sdk/src/signature.rs +++ b/sdk/src/signature.rs @@ -142,6 +142,18 @@ pub trait Signer { fn try_sign_message(&self, message: &[u8]) -> Result; } +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 { /// Return the public key for the given keypair fn pubkey(&self) -> Pubkey { @@ -170,6 +182,15 @@ where } } +impl From for Box +where + T: Signer + 'static, +{ + fn from(signer: T) -> Self { + Box::new(signer) + } +} + #[derive(Debug, Error, PartialEq)] pub enum SignerError { #[error("keypair-pubkey mismatch")] @@ -205,15 +226,14 @@ pub enum SignerError { UserCancel, } -#[derive(Debug, Default)] +#[derive(Clone, Debug, Default)] pub struct Presigner { pubkey: Pubkey, signature: Signature, } impl Presigner { - #[allow(dead_code)] - fn new(pubkey: &Pubkey, signature: &Signature) -> Self { + pub fn new(pubkey: &Pubkey, signature: &Signature) -> Self { Self { pubkey: *pubkey, signature: *signature, diff --git a/sdk/src/signers.rs b/sdk/src/signers.rs index 67b7507969..8d8997b814 100644 --- a/sdk/src/signers.rs +++ b/sdk/src/signers.rs @@ -48,6 +48,34 @@ impl Signers for [Box] { 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 Signers for [&T; 0] { default_keypairs_impl!(); } @@ -111,4 +139,22 @@ mod tests { 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()], + ); + } } diff --git a/sdk/src/transaction.rs b/sdk/src/transaction.rs index b3b11dbdcf..92b3013f7f 100644 --- a/sdk/src/transaction.rs +++ b/sdk/src/transaction.rs @@ -379,7 +379,7 @@ mod tests { use crate::{ hash::hash, instruction::AccountMeta, - signature::{Keypair, Signer}, + signature::{Keypair, Presigner, Signer}, system_instruction, }; use bincode::{deserialize, serialize, serialized_size}; @@ -671,4 +671,52 @@ mod tests { ); 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()] + ); + } }