diff --git a/Cargo.lock b/Cargo.lock index 774d1e0b25..6ead79d4c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3836,10 +3836,15 @@ dependencies = [ "bs58 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", + "console 0.7.7 (registry+https://github.com/rust-lang/crates.io-index)", "dirs 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.98 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.98 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_yaml 0.8.9 (registry+https://github.com/rust-lang/crates.io-index)", "solana 0.18.0-pre1", "solana-budget-api 0.18.0-pre1", "solana-budget-program 0.18.0-pre1", diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index 365d8aebf8..3014eb02b2 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -13,10 +13,15 @@ bincode = "1.1.4" bs58 = "0.2.0" chrono = { version = "0.4.7", features = ["serde"] } clap = "2.33.0" +console = "0.7.7" dirs = "2.0.2" +lazy_static = "1.3.0" log = "0.4.8" num-traits = "0.2" +serde = "1.0.98" +serde_derive = "1.0.98" serde_json = "1.0.40" +serde_yaml = "0.8.9" solana-budget-api = { path = "../programs/budget_api", version = "0.18.0-pre1" } solana-client = { path = "../client", version = "0.18.0-pre1" } solana-drone = { path = "../drone", version = "0.18.0-pre1" } @@ -36,4 +41,3 @@ solana-budget-program = { path = "../programs/budget_program", version = "0.18.0 [features] cuda = [] - diff --git a/wallet/src/config.rs b/wallet/src/config.rs new file mode 100644 index 0000000000..e27556f3ad --- /dev/null +++ b/wallet/src/config.rs @@ -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 = { + 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 { + 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(()) + } +} diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 2fff25cab2..6704269352 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -1 +1,5 @@ +#[macro_use] +extern crate lazy_static; + +pub mod config; pub mod wallet; diff --git a/wallet/src/main.rs b/wallet/src/main.rs index 3754880751..4a12892f6c 100644 --- a/wallet/src/main.rs +++ b/wallet/src/main.rs @@ -1,10 +1,69 @@ -use clap::{crate_description, crate_name, crate_version, Arg, ArgMatches}; +use clap::{crate_description, crate_name, crate_version, Arg, ArgGroup, ArgMatches, SubCommand}; +use console::style; use solana_sdk::signature::{gen_keypair_file, read_keypair, KeypairUtil}; +use solana_wallet::config::{self, Config}; use solana_wallet::wallet::{app, parse_command, process_command, WalletConfig, WalletError}; use std::error; +fn parse_settings(matches: &ArgMatches<'_>) -> Result> { + 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> { - let json_rpc_url = matches.value_of("json_rpc_url").unwrap().to_string(); + 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| { @@ -31,6 +90,8 @@ pub fn parse_args(matches: &ArgMatches<'_>) -> Result) -> Result Result<(), String> { match url::Url::parse(&string) { @@ -80,13 +151,25 @@ fn main() -> Result<(), Box> { 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) - .default_value(&default.json_rpc_url) .validator(is_url) .help("JSON RPC URL for the solana cluster"), ) @@ -113,10 +196,51 @@ fn main() -> Result<(), Box> { .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(); - let config = parse_args(&matches)?; - let result = process_command(&config)?; - println!("{}", result); + if parse_settings(&matches)? { + let config = parse_args(&matches)?; + let result = process_command(&config)?; + println!("{}", result); + } Ok(()) }