Rename solana-wallet program to just solana (#5604)

* Rename wallet/ to cli/

* Rename the solana-wallet crate to solana-cli

* Rename solana-wallet program to solana

* cargo fmt
This commit is contained in:
Michael Vines
2019-08-22 13:51:16 -07:00
committed by GitHub
parent 19d16e75c6
commit 1207664bbb
29 changed files with 162 additions and 145 deletions

2
cli/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target/
/farf/

50
cli/Cargo.toml Normal file
View File

@ -0,0 +1,50 @@
[package]
authors = ["Solana Maintainers <maintainers@solana.com>"]
edition = "2018"
name = "solana-cli"
description = "Blockchain, Rebuilt for Scale"
version = "0.18.0-pre2"
repository = "https://github.com/solana-labs/solana"
license = "Apache-2.0"
homepage = "https://solana.com/"
[dependencies]
bincode = "1.1.4"
bs58 = "0.2.4"
chrono = { version = "0.4.7", features = ["serde"] }
clap = "2.33.0"
criterion-stats = "0.3.0"
ctrlc = { version = "3.1.3", features = ["termination"] }
console = "0.7.7"
dirs = "2.0.2"
lazy_static = "1.3.0"
log = "0.4.8"
num-traits = "0.2"
pretty-hex = "0.1.0"
serde = "1.0.99"
serde_derive = "1.0.99"
serde_json = "1.0.40"
serde_yaml = "0.8.9"
solana-budget-api = { path = "../programs/budget_api", version = "0.18.0-pre2" }
solana-client = { path = "../client", version = "0.18.0-pre2" }
solana-drone = { path = "../drone", version = "0.18.0-pre2" }
solana-logger = { path = "../logger", version = "0.18.0-pre2" }
solana-netutil = { path = "../utils/netutil", version = "0.18.0-pre2" }
solana-runtime = { path = "../runtime", version = "0.18.0-pre2" }
solana-sdk = { path = "../sdk", version = "0.18.0-pre2" }
solana-stake-api = { path = "../programs/stake_api", version = "0.18.0-pre2" }
solana-storage-api = { path = "../programs/storage_api", version = "0.18.0-pre2" }
solana-vote-api = { path = "../programs/vote_api", version = "0.18.0-pre2" }
solana-vote-signer = { path = "../vote-signer", version = "0.18.0-pre2" }
url = "2.1.0"
[dev-dependencies]
solana-core = { path = "../core", version = "0.18.0-pre2" }
solana-budget-program = { path = "../programs/budget_program", version = "0.18.0-pre2" }
[features]
cuda = []
[[bin]]
name = "solana"
path = "src/main.rs"

21
cli/cli-help.sh Executable file
View File

@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")"/..
cargo build --package solana-cli
export PATH=$PWD/target/debug:$PATH
echo "\`\`\`manpage"
solana --help
echo "\`\`\`"
echo ""
commands=(address airdrop balance cancel confirm deploy fees get-transaction-count pay send-signature send-timestamp)
for x in "${commands[@]}"; do
echo "\`\`\`manpage"
solana "${x}" --help
echo "\`\`\`"
echo ""
done

49
cli/src/config.rs Normal file
View File

@ -0,0 +1,49 @@
// Wallet settings that can be configured for long-term use
use serde_derive::{Deserialize, Serialize};
use std::fs::{create_dir_all, File};
use std::io::{self, Write};
use std::path::Path;
lazy_static! {
pub static ref CONFIG_FILE: Option<String> = {
dirs::home_dir().map(|mut path| {
path.extend(&[".config", "solana", "wallet", "config.yml"]);
path.to_str().unwrap().to_string()
})
};
}
#[derive(Serialize, Deserialize, Default, Debug, PartialEq)]
pub struct Config {
pub url: String,
pub keypair: String,
}
impl Config {
pub fn new(url: &str, keypair: &str) -> Self {
Self {
url: url.to_string(),
keypair: keypair.to_string(),
}
}
pub fn load(config_file: &str) -> Result<Self, io::Error> {
let file = File::open(config_file.to_string())?;
let config = serde_yaml::from_reader(file)
.map_err(|err| io::Error::new(io::ErrorKind::Other, format!("{:?}", err)))?;
Ok(config)
}
pub fn save(&self, config_file: &str) -> Result<(), io::Error> {
let serialized = serde_yaml::to_string(self)
.map_err(|err| io::Error::new(io::ErrorKind::Other, format!("{:?}", err)))?;
if let Some(outdir) = Path::new(&config_file).parent() {
create_dir_all(outdir)?;
}
let mut file = File::create(config_file)?;
file.write_all(&serialized.into_bytes())?;
Ok(())
}
}

11
cli/src/display.rs Normal file
View File

@ -0,0 +1,11 @@
use console::style;
// Pretty print a "name value"
pub fn println_name_value(name: &str, value: &str) {
let styled_value = if value == "" {
style("(not set)").italic()
} else {
style(value)
};
println!("{} {}", style(name).bold(), styled_value);
}

6
cli/src/lib.rs Normal file
View File

@ -0,0 +1,6 @@
#[macro_use]
extern crate lazy_static;
pub mod config;
pub mod display;
pub mod wallet;

237
cli/src/main.rs Normal file
View File

@ -0,0 +1,237 @@
use clap::{crate_description, crate_name, crate_version, Arg, ArgGroup, ArgMatches, SubCommand};
use console::style;
use solana_cli::config::{self, Config};
use solana_cli::display::println_name_value;
use solana_cli::wallet::{app, parse_command, process_command, WalletConfig, WalletError};
use solana_sdk::signature::{gen_keypair_file, read_keypair, KeypairUtil};
use std::error;
fn parse_settings(matches: &ArgMatches<'_>) -> Result<bool, Box<dyn error::Error>> {
let parse_args = match matches.subcommand() {
("get", Some(subcommand_matches)) => {
if let Some(config_file) = matches.value_of("config_file") {
let config = Config::load(config_file).unwrap_or_default();
if let Some(field) = subcommand_matches.value_of("specific_setting") {
let value = match field {
"url" => config.url,
"keypair" => config.keypair,
_ => unreachable!(),
};
println_name_value(&format!("* {}:", field), &value);
} else {
println_name_value("Wallet Config:", config_file);
println_name_value("* url:", &config.url);
println_name_value("* keypair:", &config.keypair);
}
} else {
println!("{} Either provide the `--config` arg or ensure home directory exists to use the default config location", style("No config file found.").bold());
}
false
}
("set", Some(subcommand_matches)) => {
if let Some(config_file) = matches.value_of("config_file") {
let mut config = Config::load(config_file).unwrap_or_default();
if let Some(url) = subcommand_matches.value_of("url") {
config.url = url.to_string();
}
if let Some(keypair) = subcommand_matches.value_of("keypair") {
config.keypair = keypair.to_string();
}
config.save(config_file)?;
println_name_value("Wallet Config Updated:", config_file);
println_name_value("* url:", &config.url);
println_name_value("* keypair:", &config.keypair);
} else {
println!("{} Either provide the `--config` arg or ensure home directory exists to use the default config location", style("No config file found.").bold());
}
false
}
_ => true,
};
Ok(parse_args)
}
pub fn parse_args(matches: &ArgMatches<'_>) -> Result<WalletConfig, Box<dyn error::Error>> {
let config = if let Some(config_file) = matches.value_of("config_file") {
Config::load(config_file).unwrap_or_default()
} else {
Config::default()
};
let json_rpc_url = if let Some(url) = matches.value_of("json_rpc_url") {
url.to_string()
} else if config.url != "" {
config.url
} else {
let default = WalletConfig::default();
default.json_rpc_url
};
let drone_host = if let Some(drone_host) = matches.value_of("drone_host") {
Some(solana_netutil::parse_host(drone_host).or_else(|err| {
Err(WalletError::BadParameter(format!(
"Invalid drone host: {:?}",
err
)))
})?)
} else {
None
};
let drone_port = matches
.value_of("drone_port")
.unwrap()
.parse()
.or_else(|err| {
Err(WalletError::BadParameter(format!(
"Invalid drone port: {:?}",
err
)))
})?;
let mut path = dirs::home_dir().expect("home directory");
let id_path = if matches.is_present("keypair") {
matches.value_of("keypair").unwrap()
} else if config.keypair != "" {
&config.keypair
} else {
path.extend(&[".config", "solana", "id.json"]);
if !path.exists() {
gen_keypair_file(path.to_str().unwrap())?;
println!("New keypair generated at: {}", path.to_str().unwrap());
}
path.to_str().unwrap()
};
let keypair = read_keypair(id_path).or_else(|err| {
Err(WalletError::BadParameter(format!(
"{}: Unable to open keypair file: {}",
err, id_path
)))
})?;
let command = parse_command(&keypair.pubkey(), &matches)?;
Ok(WalletConfig {
command,
drone_host,
drone_port,
json_rpc_url,
keypair,
rpc_client: None,
})
}
// Return an error if a url cannot be parsed.
fn is_url(string: String) -> Result<(), String> {
match url::Url::parse(&string) {
Ok(url) => {
if url.has_host() {
Ok(())
} else {
Err("no host provided".to_string())
}
}
Err(err) => Err(format!("{:?}", err)),
}
}
fn main() -> Result<(), Box<dyn error::Error>> {
solana_logger::setup();
let default = WalletConfig::default();
let default_drone_port = format!("{}", default.drone_port);
let matches = app(crate_name!(), crate_description!(), crate_version!())
.arg({
let arg = Arg::with_name("config_file")
.short("c")
.long("config")
.value_name("PATH")
.takes_value(true)
.help("Configuration file to use");
if let Some(ref config_file) = *config::CONFIG_FILE {
arg.default_value(&config_file)
} else {
arg
}
})
.arg(
Arg::with_name("json_rpc_url")
.short("u")
.long("url")
.value_name("URL")
.takes_value(true)
.validator(is_url)
.help("JSON RPC URL for the solana cluster"),
)
.arg(
Arg::with_name("drone_host")
.long("drone-host")
.value_name("HOST")
.takes_value(true)
.help("Drone host to use [default: same as the --url host]"),
)
.arg(
Arg::with_name("drone_port")
.long("drone-port")
.value_name("PORT")
.takes_value(true)
.default_value(&default_drone_port)
.help("Drone port to use"),
)
.arg(
Arg::with_name("keypair")
.short("k")
.long("keypair")
.value_name("PATH")
.takes_value(true)
.help("/path/to/id.json"),
)
.subcommand(
SubCommand::with_name("get")
.about("Get wallet config settings")
.arg(
Arg::with_name("specific_setting")
.index(1)
.value_name("CONFIG_FIELD")
.takes_value(true)
.possible_values(&["url", "keypair"])
.help("Return a specific config setting"),
),
)
.subcommand(
SubCommand::with_name("set")
.about("Set a wallet config setting")
.arg(
Arg::with_name("url")
.short("u")
.long("url")
.value_name("URL")
.takes_value(true)
.validator(is_url)
.help("Set default JSON RPC URL to query"),
)
.arg(
Arg::with_name("keypair")
.short("k")
.long("keypair")
.value_name("PATH")
.takes_value(true)
.help("/path/to/id.json"),
)
.group(
ArgGroup::with_name("config_settings")
.args(&["url", "keypair"])
.multiple(true)
.required(true),
),
)
.get_matches();
if parse_settings(&matches)? {
let config = parse_args(&matches)?;
let result = process_command(&config)?;
println!("{}", result);
}
Ok(())
}

2766
cli/src/wallet.rs Normal file

File diff suppressed because it is too large Load Diff

80
cli/tests/deploy.rs Normal file
View File

@ -0,0 +1,80 @@
use serde_json::{json, Value};
use solana_cli::wallet::{process_command, WalletCommand, WalletConfig};
use solana_client::rpc_client::RpcClient;
use solana_client::rpc_request::RpcRequest;
use solana_core::validator::new_validator_for_tests;
use solana_drone::drone::run_local_drone;
use solana_sdk::bpf_loader;
use std::fs::{remove_dir_all, File};
use std::io::Read;
use std::path::PathBuf;
use std::sync::mpsc::channel;
#[test]
fn test_wallet_deploy_program() {
solana_logger::setup();
let mut pathbuf = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
pathbuf.push("tests");
pathbuf.push("fixtures");
pathbuf.push("noop");
pathbuf.set_extension("so");
let (server, leader_data, alice, ledger_path) = new_validator_for_tests();
let (sender, receiver) = channel();
run_local_drone(alice, sender, None);
let drone_addr = receiver.recv().unwrap();
let rpc_client = RpcClient::new_socket(leader_data.rpc);
let mut config = WalletConfig::default();
config.drone_port = drone_addr.port();
config.json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port());
config.command = WalletCommand::Airdrop(50);
process_command(&config).unwrap();
config.command = WalletCommand::Deploy(pathbuf.to_str().unwrap().to_string());
let response = process_command(&config);
let json: Value = serde_json::from_str(&response.unwrap()).unwrap();
let program_id_str = json
.as_object()
.unwrap()
.get("programId")
.unwrap()
.as_str()
.unwrap();
let params = json!([program_id_str]);
let account_info = rpc_client
.retry_make_rpc_request(&RpcRequest::GetAccountInfo, Some(params), 0)
.unwrap();
let account_info_obj = account_info.as_object().unwrap();
assert_eq!(
account_info_obj.get("lamports").unwrap().as_u64().unwrap(),
1
);
let owner_array = account_info.get("owner").unwrap();
assert_eq!(owner_array, &json!(bpf_loader::id()));
assert_eq!(
account_info_obj
.get("executable")
.unwrap()
.as_bool()
.unwrap(),
true
);
let mut file = File::open(pathbuf.to_str().unwrap().to_string()).unwrap();
let mut elf = Vec::new();
file.read_to_end(&mut elf).unwrap();
assert_eq!(
account_info_obj.get("data").unwrap().as_array().unwrap(),
&elf
);
server.close().unwrap();
remove_dir_all(ledger_path).unwrap();
}

7
cli/tests/fixtures/build.sh vendored Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -ex
cd "$(dirname "$0")"
make -C ../../../programs/bpf/c/
cp ../../../programs/bpf/c/out/noop.so .

BIN
cli/tests/fixtures/noop.so vendored Executable file

Binary file not shown.

233
cli/tests/pay.rs Normal file
View File

@ -0,0 +1,233 @@
use chrono::prelude::*;
use serde_json::Value;
use solana_cli::wallet::{
process_command, request_and_confirm_airdrop, WalletCommand, WalletConfig,
};
use solana_client::rpc_client::RpcClient;
use solana_drone::drone::run_local_drone;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::KeypairUtil;
use std::fs::remove_dir_all;
use std::sync::mpsc::channel;
#[cfg(test)]
use solana_core::validator::new_validator_for_tests;
use std::thread::sleep;
use std::time::Duration;
fn check_balance(expected_balance: u64, client: &RpcClient, pubkey: &Pubkey) {
(0..5).for_each(|tries| {
let balance = client.retry_get_balance(pubkey, 1).unwrap().unwrap();
if balance == expected_balance {
return;
}
if tries == 4 {
assert_eq!(balance, expected_balance);
}
sleep(Duration::from_millis(500));
});
}
#[test]
fn test_wallet_timestamp_tx() {
let (server, leader_data, alice, ledger_path) = new_validator_for_tests();
let bob_pubkey = Pubkey::new_rand();
let (sender, receiver) = channel();
run_local_drone(alice, sender, None);
let drone_addr = receiver.recv().unwrap();
let rpc_client = RpcClient::new_socket(leader_data.rpc);
let mut config_payer = WalletConfig::default();
config_payer.drone_port = drone_addr.port();
config_payer.json_rpc_url =
format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port());
let mut config_witness = WalletConfig::default();
config_witness.drone_port = config_payer.drone_port;
config_witness.json_rpc_url = config_payer.json_rpc_url.clone();
assert_ne!(
config_payer.keypair.pubkey(),
config_witness.keypair.pubkey()
);
request_and_confirm_airdrop(&rpc_client, &drone_addr, &config_payer.keypair.pubkey(), 50)
.unwrap();
check_balance(50, &rpc_client, &config_payer.keypair.pubkey());
request_and_confirm_airdrop(
&rpc_client,
&drone_addr,
&config_witness.keypair.pubkey(),
1,
)
.unwrap();
// Make transaction (from config_payer to bob_pubkey) requiring timestamp from config_witness
let date_string = "\"2018-09-19T17:30:59Z\"";
let dt: DateTime<Utc> = serde_json::from_str(&date_string).unwrap();
config_payer.command = WalletCommand::Pay(
10,
bob_pubkey,
Some(dt),
Some(config_witness.keypair.pubkey()),
None,
None,
);
let sig_response = process_command(&config_payer);
let object: Value = serde_json::from_str(&sig_response.unwrap()).unwrap();
let process_id_str = object.get("processId").unwrap().as_str().unwrap();
let process_id_vec = bs58::decode(process_id_str)
.into_vec()
.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(10, &rpc_client, &process_id); // contract balance
check_balance(0, &rpc_client, &bob_pubkey); // recipient balance
// Sign transaction by config_witness
config_witness.command = WalletCommand::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(0, &rpc_client, &process_id); // contract balance
check_balance(10, &rpc_client, &bob_pubkey); // recipient balance
server.close().unwrap();
remove_dir_all(ledger_path).unwrap();
}
#[test]
fn test_wallet_witness_tx() {
let (server, leader_data, alice, ledger_path) = new_validator_for_tests();
let bob_pubkey = Pubkey::new_rand();
let (sender, receiver) = channel();
run_local_drone(alice, sender, None);
let drone_addr = receiver.recv().unwrap();
let rpc_client = RpcClient::new_socket(leader_data.rpc);
let mut config_payer = WalletConfig::default();
config_payer.drone_port = drone_addr.port();
config_payer.json_rpc_url =
format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port());
let mut config_witness = WalletConfig::default();
config_witness.drone_port = config_payer.drone_port;
config_witness.json_rpc_url = config_payer.json_rpc_url.clone();
assert_ne!(
config_payer.keypair.pubkey(),
config_witness.keypair.pubkey()
);
request_and_confirm_airdrop(&rpc_client, &drone_addr, &config_payer.keypair.pubkey(), 50)
.unwrap();
request_and_confirm_airdrop(
&rpc_client,
&drone_addr,
&config_witness.keypair.pubkey(),
1,
)
.unwrap();
// Make transaction (from config_payer to bob_pubkey) requiring witness signature from config_witness
config_payer.command = WalletCommand::Pay(
10,
bob_pubkey,
None,
None,
Some(vec![config_witness.keypair.pubkey()]),
None,
);
let sig_response = process_command(&config_payer);
let object: Value = serde_json::from_str(&sig_response.unwrap()).unwrap();
let process_id_str = object.get("processId").unwrap().as_str().unwrap();
let process_id_vec = bs58::decode(process_id_str)
.into_vec()
.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(10, &rpc_client, &process_id); // contract balance
check_balance(0, &rpc_client, &bob_pubkey); // recipient balance
// Sign transaction by config_witness
config_witness.command = WalletCommand::Witness(bob_pubkey, process_id);
process_command(&config_witness).unwrap();
check_balance(40, &rpc_client, &config_payer.keypair.pubkey()); // config_payer balance
check_balance(0, &rpc_client, &process_id); // contract balance
check_balance(10, &rpc_client, &bob_pubkey); // recipient balance
server.close().unwrap();
remove_dir_all(ledger_path).unwrap();
}
#[test]
fn test_wallet_cancel_tx() {
let (server, leader_data, alice, ledger_path) = new_validator_for_tests();
let bob_pubkey = Pubkey::new_rand();
let (sender, receiver) = channel();
run_local_drone(alice, sender, None);
let drone_addr = receiver.recv().unwrap();
let rpc_client = RpcClient::new_socket(leader_data.rpc);
let mut config_payer = WalletConfig::default();
config_payer.drone_port = drone_addr.port();
config_payer.json_rpc_url =
format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port());
let mut config_witness = WalletConfig::default();
config_witness.drone_port = config_payer.drone_port;
config_witness.json_rpc_url = config_payer.json_rpc_url.clone();
assert_ne!(
config_payer.keypair.pubkey(),
config_witness.keypair.pubkey()
);
request_and_confirm_airdrop(&rpc_client, &drone_addr, &config_payer.keypair.pubkey(), 50)
.unwrap();
// Make transaction (from config_payer to bob_pubkey) requiring witness signature from config_witness
config_payer.command = WalletCommand::Pay(
10,
bob_pubkey,
None,
None,
Some(vec![config_witness.keypair.pubkey()]),
Some(config_payer.keypair.pubkey()),
);
let sig_response = process_command(&config_payer).unwrap();
let object: Value = serde_json::from_str(&sig_response).unwrap();
let process_id_str = object.get("processId").unwrap().as_str().unwrap();
let process_id_vec = bs58::decode(process_id_str)
.into_vec()
.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(10, &rpc_client, &process_id); // contract balance
check_balance(0, &rpc_client, &bob_pubkey); // recipient balance
// Sign transaction by config_witness
config_payer.command = WalletCommand::Cancel(process_id);
process_command(&config_payer).unwrap();
check_balance(50, &rpc_client, &config_payer.keypair.pubkey()); // config_payer balance
check_balance(0, &rpc_client, &process_id); // contract balance
check_balance(0, &rpc_client, &bob_pubkey); // recipient balance
server.close().unwrap();
remove_dir_all(ledger_path).unwrap();
}

View File

@ -0,0 +1,35 @@
use solana_cli::wallet::{process_command, WalletCommand, WalletConfig};
use solana_client::rpc_client::RpcClient;
use solana_core::validator::new_validator_for_tests;
use solana_drone::drone::run_local_drone;
use solana_sdk::signature::KeypairUtil;
use std::fs::remove_dir_all;
use std::sync::mpsc::channel;
#[test]
fn test_wallet_request_airdrop() {
let (server, leader_data, alice, ledger_path) = new_validator_for_tests();
let (sender, receiver) = channel();
run_local_drone(alice, sender, None);
let drone_addr = receiver.recv().unwrap();
let mut bob_config = WalletConfig::default();
bob_config.drone_port = drone_addr.port();
bob_config.json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port());
bob_config.command = WalletCommand::Airdrop(50);
let sig_response = process_command(&bob_config);
sig_response.unwrap();
let rpc_client = RpcClient::new_socket(leader_data.rpc);
let balance = rpc_client
.retry_get_balance(&bob_config.keypair.pubkey(), 1)
.unwrap()
.unwrap();
assert_eq!(balance, 50);
server.close().unwrap();
remove_dir_all(ledger_path).unwrap();
}