grin_wallet/cli/
cli.rs

1// Copyright 2021 The Grin Developers
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use crate::cmd::wallet_args;
16use crate::util::secp::key::SecretKey;
17use crate::util::Mutex;
18use clap::App;
19//use colored::Colorize;
20use grin_keychain as keychain;
21use grin_wallet_api::Owner;
22use grin_wallet_config::{TorConfig, WalletConfig};
23use grin_wallet_controller::command::GlobalArgs;
24use grin_wallet_controller::Error;
25use grin_wallet_impls::DefaultWalletImpl;
26use grin_wallet_libwallet::{NodeClient, StatusMessage, WalletInst, WalletLCProvider};
27use rustyline::completion::{Completer, FilenameCompleter, Pair};
28use rustyline::error::ReadlineError;
29use rustyline::highlight::{Highlighter, MatchingBracketHighlighter};
30use rustyline::hint::Hinter;
31use rustyline::validate::Validator;
32use rustyline::{CompletionType, Config, Context, EditMode, Editor, Helper, OutputStreamType};
33use std::borrow::Cow::{self, Borrowed, Owned};
34use std::sync::mpsc::{channel, Receiver};
35use std::sync::Arc;
36use std::thread;
37use std::time::Duration;
38
39const COLORED_PROMPT: &'static str = "\x1b[36mgrin-wallet>\x1b[0m ";
40const PROMPT: &'static str = "grin-wallet> ";
41//const HISTORY_PATH: &str = ".history";
42
43// static for keeping track of current stdin buffer contents
44lazy_static! {
45	static ref STDIN_CONTENTS: Mutex<String> = Mutex::new(String::from(""));
46}
47
48#[macro_export]
49macro_rules! cli_message_inline {
50	($fmt_string:expr, $( $arg:expr ),+) => {
51			{
52					use std::io::Write;
53					let contents = STDIN_CONTENTS.lock();
54					/* use crate::common::{is_cli, COLORED_PROMPT}; */
55					/* if is_cli() { */
56							print!("\r");
57							print!($fmt_string, $( $arg ),*);
58							print!(" {}", COLORED_PROMPT);
59							print!("\x1B[J");
60							print!("{}", *contents);
61							std::io::stdout().flush().unwrap();
62					/*} else {
63							info!($fmt_string, $( $arg ),*);
64					}*/
65			}
66	};
67}
68
69#[macro_export]
70macro_rules! cli_message {
71	($fmt_string:expr, $( $arg:expr ),+) => {
72			{
73					use std::io::Write;
74					/* use crate::common::{is_cli, COLORED_PROMPT}; */
75					/* if is_cli() { */
76							//print!("\r");
77							print!($fmt_string, $( $arg ),*);
78							println!();
79							std::io::stdout().flush().unwrap();
80					/*} else {
81							info!($fmt_string, $( $arg ),*);
82					}*/
83			}
84	};
85}
86
87/// function to catch updates
88pub fn start_updater_thread(rx: Receiver<StatusMessage>) -> Result<(), Error> {
89	let _ = thread::Builder::new()
90		.name("wallet-updater-status".to_string())
91		.spawn(move || loop {
92			while let Ok(m) = rx.recv() {
93				match m {
94					StatusMessage::UpdatingOutputs(s) => cli_message_inline!("{}", s),
95					StatusMessage::UpdatingTransactions(s) => cli_message_inline!("{}", s),
96					StatusMessage::FullScanWarn(s) => cli_message_inline!("{}", s),
97					StatusMessage::Scanning(_, m) => {
98						//debug!("{}", s);
99						cli_message_inline!("Scanning - {}% complete - Please Wait", m);
100					}
101					StatusMessage::ScanningComplete(s) => cli_message_inline!("{}", s),
102					StatusMessage::UpdateWarning(s) => cli_message_inline!("{}", s),
103				}
104			}
105		});
106	Ok(())
107}
108
109pub fn command_loop<L, C, K>(
110	wallet_inst: Arc<Mutex<Box<dyn WalletInst<'static, L, C, K>>>>,
111	keychain_mask: Option<SecretKey>,
112	wallet_config: &WalletConfig,
113	tor_config: &TorConfig,
114	global_wallet_args: &GlobalArgs,
115	test_mode: bool,
116) -> Result<(), Error>
117where
118	DefaultWalletImpl<'static, C>: WalletInst<'static, L, C, K>,
119	L: WalletLCProvider<'static, C, K> + 'static,
120	C: NodeClient + 'static,
121	K: keychain::Keychain + 'static,
122{
123	let editor = Config::builder()
124		.history_ignore_space(true)
125		.completion_type(CompletionType::List)
126		.edit_mode(EditMode::Emacs)
127		.output_stream(OutputStreamType::Stdout)
128		.build();
129
130	let mut reader = Editor::with_config(editor);
131	reader.set_helper(Some(EditorHelper(
132		FilenameCompleter::new(),
133		MatchingBracketHighlighter::new(),
134	)));
135
136	/*let history_file = self
137		.api
138		.config()
139		.get_data_path()
140		.unwrap()
141		.parent()
142		.unwrap()
143		.join(HISTORY_PATH);
144	if history_file.exists() {
145		let _ = reader.load_history(&history_file);
146	}*/
147
148	let yml = load_yaml!("../bin/grin-wallet.yml");
149	let mut app = App::from_yaml(yml).version(crate_version!());
150	let mut keychain_mask = keychain_mask;
151
152	// catch updater messages
153	let (tx, rx) = channel();
154	let mut owner_api = Owner::new(wallet_inst, Some(tx));
155	start_updater_thread(rx)?;
156
157	// start the automatic updater
158	owner_api.start_updater((&keychain_mask).as_ref(), Duration::from_secs(30))?;
159	let mut wallet_opened = false;
160	loop {
161		match reader.readline(PROMPT) {
162			Ok(command) => {
163				if command.is_empty() {
164					continue;
165				}
166				// TODO tidy up a bit
167				if command.to_lowercase() == "exit" {
168					break;
169				}
170				/* use crate::common::{is_cli, COLORED_PROMPT}; */
171
172				// reset buffer
173				{
174					let mut contents = STDIN_CONTENTS.lock();
175					*contents = String::from("");
176				}
177
178				// Just add 'grin-wallet' to each command behind the scenes
179				// so we don't need to maintain a separate definition file
180				let augmented_command = format!("grin-wallet {}", command);
181				let args =
182					app.get_matches_from_safe_borrow(augmented_command.trim().split_whitespace());
183				let done = match args {
184					Ok(args) => {
185						// handle opening /closing separately
186						keychain_mask = match args.subcommand() {
187							("open", Some(_)) => {
188								let mut wallet_lock = owner_api.wallet_inst.lock();
189								let lc = wallet_lock.lc_provider().unwrap();
190								let mask = match lc.open_wallet(
191									None,
192									wallet_args::prompt_password(&global_wallet_args.password),
193									false,
194									false,
195								) {
196									Ok(m) => {
197										wallet_opened = true;
198										m
199									}
200									Err(e) => {
201										cli_message!("{}", e);
202										None
203									}
204								};
205								if let Some(account) = args.value_of("account") {
206									if wallet_opened {
207										let wallet_inst = lc.wallet_inst()?;
208										wallet_inst.set_parent_key_id_by_name(account)?;
209									}
210								}
211								mask
212							}
213							("close", Some(_)) => {
214								let mut wallet_lock = owner_api.wallet_inst.lock();
215								let lc = wallet_lock.lc_provider().unwrap();
216								lc.close_wallet(None)?;
217								None
218							}
219							_ => keychain_mask,
220						};
221						match wallet_args::parse_and_execute(
222							&mut owner_api,
223							keychain_mask.clone(),
224							&wallet_config,
225							&tor_config,
226							&global_wallet_args,
227							&args,
228							test_mode,
229							true,
230						) {
231							Ok(_) => {
232								cli_message!("Command '{}' completed", args.subcommand().0);
233								false
234							}
235							Err(err) => {
236								cli_message!("{}", err);
237								false
238							}
239						}
240					}
241					Err(err) => {
242						cli_message!("{}", err);
243						false
244					}
245				};
246				reader.add_history_entry(command);
247				if done {
248					println!();
249					break;
250				}
251			}
252			Err(err) => {
253				println!("Unable to read line: {}", err);
254				break;
255			}
256		}
257	}
258	Ok(())
259
260	//let _ = reader.save_history(&history_file);
261}
262
263struct EditorHelper(FilenameCompleter, MatchingBracketHighlighter);
264
265impl Completer for EditorHelper {
266	type Candidate = Pair;
267
268	fn complete(
269		&self,
270		line: &str,
271		pos: usize,
272		ctx: &Context<'_>,
273	) -> std::result::Result<(usize, Vec<Pair>), ReadlineError> {
274		self.0.complete(line, pos, ctx)
275	}
276}
277
278impl Hinter for EditorHelper {
279	fn hint(&self, line: &str, _pos: usize, _ctx: &Context<'_>) -> Option<String> {
280		let mut contents = STDIN_CONTENTS.lock();
281		*contents = line.into();
282		None
283	}
284}
285
286impl Highlighter for EditorHelper {
287	fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> {
288		self.1.highlight(line, pos)
289	}
290
291	fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
292		&'s self,
293		prompt: &'p str,
294		default: bool,
295	) -> Cow<'b, str> {
296		if default {
297			Borrowed(COLORED_PROMPT)
298		} else {
299			Borrowed(prompt)
300		}
301	}
302
303	fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
304		Owned("\x1b[1m".to_owned() + hint + "\x1b[m")
305	}
306
307	fn highlight_char(&self, line: &str, pos: usize) -> bool {
308		self.1.highlight_char(line, pos)
309	}
310}
311impl Validator for EditorHelper {}
312impl Helper for EditorHelper {}