1use crate::error::Error;
2use crate::helpers::*;
3use crate::imports::*;
4use crate::modules::miner::Miner;
5use crate::modules::node::Node;
6use crate::notifier::{Notification, Notifier};
7use crate::result::Result;
8use kaspa_daemon::{DaemonEvent, DaemonKind, Daemons};
9use kaspa_wallet_core::account::Account;
10use kaspa_wallet_core::rpc::DynRpcApi;
11use kaspa_wallet_core::storage::{IdT, PrvKeyDataInfo};
12use kaspa_wrpc_client::{KaspaRpcClient, Resolver};
13use workflow_core::channel::*;
14use workflow_core::time::Instant;
15use workflow_log::*;
16pub use workflow_terminal::Event as TerminalEvent;
17use workflow_terminal::*;
18pub use workflow_terminal::{Options as TerminalOptions, TargetElement as TerminalTarget};
19
20const NOTIFY: &str = "\x1B[2m⎟\x1B[0m";
21
22pub struct Options {
23 pub daemons: Option<Arc<Daemons>>,
24 pub terminal: TerminalOptions,
25}
26
27impl Options {
28 pub fn new(terminal_options: TerminalOptions, daemons: Option<Arc<Daemons>>) -> Self {
29 Self { daemons, terminal: terminal_options }
30 }
31}
32
33pub struct KaspaCli {
34 term: Arc<Mutex<Option<Arc<Terminal>>>>,
35 wallet: Arc<Wallet>,
36 notifications_task_ctl: DuplexChannel,
37 mute: Arc<AtomicBool>,
38 flags: Flags,
39 last_interaction: Arc<Mutex<Instant>>,
40 daemons: Arc<Daemons>,
41 handlers: Arc<HandlerCli>,
42 shutdown: Arc<AtomicBool>,
43 node: Mutex<Option<Arc<Node>>>,
44 miner: Mutex<Option<Arc<Miner>>>,
45 notifier: Notifier,
46 sync_state: Mutex<Option<SyncState>>,
47}
48
49impl From<&KaspaCli> for Arc<Terminal> {
50 fn from(ctx: &KaspaCli) -> Arc<Terminal> {
51 ctx.term()
52 }
53}
54
55impl AsRef<KaspaCli> for KaspaCli {
56 fn as_ref(&self) -> &Self {
57 self
58 }
59}
60
61impl workflow_log::Sink for KaspaCli {
62 fn write(&self, _target: Option<&str>, _level: Level, args: &std::fmt::Arguments<'_>) -> bool {
63 if let Some(term) = self.try_term() {
64 cfg_if! {
65 if #[cfg(target_arch = "wasm32")] {
66 if _level == Level::Error {
67 term.writeln(style(args.to_string().crlf()).red().to_string());
68 }
69 false
70 } else {
71 match _level {
72 Level::Error => {
73 term.writeln(style(args.to_string().crlf()).red().to_string());
74 },
75 _ => {
76 term.writeln(args.to_string());
77 }
78 }
79 true
80 }
81 }
82 } else {
83 false
84 }
85 }
86}
87
88impl KaspaCli {
89 pub fn init() {
90 cfg_if! {
91 if #[cfg(not(target_arch = "wasm32"))] {
92 init_panic_hook(||{
93 std::println!("halt");
94 1
95 });
96 kaspa_core::log::init_logger(None, "info");
97 } else {
98 kaspa_core::log::set_log_level(LevelFilter::Info);
99 }
100 }
101
102 workflow_log::set_colors_enabled(true);
103 }
104
105 pub async fn try_new_arc(options: Options) -> Result<Arc<Self>> {
106 let wallet = Arc::new(Wallet::try_new(Wallet::local_store()?, Some(Resolver::default()), None)?);
107
108 let kaspa_cli = Arc::new(KaspaCli {
109 term: Arc::new(Mutex::new(None)),
110 wallet,
111 notifications_task_ctl: DuplexChannel::oneshot(),
112 mute: Arc::new(AtomicBool::new(true)),
113 flags: Flags::default(),
114 last_interaction: Arc::new(Mutex::new(Instant::now())),
115 handlers: Arc::new(HandlerCli::default()),
116 daemons: options.daemons.unwrap_or_default(),
117 shutdown: Arc::new(AtomicBool::new(false)),
118 node: Mutex::new(None),
119 miner: Mutex::new(None),
120 notifier: Notifier::try_new()?,
121 sync_state: Mutex::new(None),
122 });
123
124 let term = Arc::new(Terminal::try_new_with_options(kaspa_cli.clone(), options.terminal)?);
125 term.init().await?;
126
127 cfg_if! {
128 if #[cfg(target_arch = "wasm32")] {
129 kaspa_cli.init_panic_hook();
130 }
131 }
132
133 Ok(kaspa_cli)
134 }
135
136 pub fn term(&self) -> Arc<Terminal> {
137 self.term.lock().unwrap().as_ref().cloned().expect("WalletCli::term is not initialized")
138 }
139
140 pub fn try_term(&self) -> Option<Arc<Terminal>> {
141 self.term.lock().unwrap().as_ref().cloned()
142 }
143
144 pub fn notifier(&self) -> &Notifier {
145 &self.notifier
146 }
147
148 pub fn version(&self) -> String {
149 env!("CARGO_PKG_VERSION").to_string()
150 }
151
152 pub fn wallet(&self) -> Arc<Wallet> {
153 self.wallet.clone()
154 }
155
156 pub fn is_connected(&self) -> bool {
157 self.wallet.is_connected()
158 }
159
160 pub fn rpc_api(&self) -> Arc<DynRpcApi> {
161 self.wallet.rpc_api().clone()
162 }
163
164 pub fn try_rpc_api(&self) -> Option<Arc<DynRpcApi>> {
165 self.wallet.try_rpc_api().clone()
166 }
167
168 pub fn try_rpc_client(&self) -> Option<Arc<KaspaRpcClient>> {
169 self.wallet.try_wrpc_client().clone()
170 }
171
172 pub fn store(&self) -> Arc<dyn Interface> {
173 self.wallet.store().clone()
174 }
175
176 pub fn daemons(&self) -> &Arc<Daemons> {
177 &self.daemons
178 }
179
180 pub fn handlers(&self) -> Arc<HandlerCli> {
181 self.handlers.clone()
182 }
183
184 pub fn flags(&self) -> &Flags {
185 &self.flags
186 }
187
188 pub fn toggle_mute(&self) -> &'static str {
189 helpers::toggle(&self.mute)
190 }
191
192 pub fn is_mutted(&self) -> bool {
193 self.mute.load(Ordering::SeqCst)
194 }
195
196 pub fn register_metrics(self: &Arc<Self>) -> Result<()> {
197 use crate::modules::metrics;
198 register_handlers!(self, self.handlers(), [metrics]);
199 Ok(())
200 }
201
202 pub fn register_handlers(self: &Arc<Self>) -> Result<()> {
203 crate::modules::register_handlers(self)?;
204
205 if let Some(node) = self.handlers().get("node") {
206 let node = node.downcast_arc::<crate::modules::node::Node>().ok();
207 *self.node.lock().unwrap() = node;
208 }
209
210 if let Some(miner) = self.handlers().get("miner") {
211 let miner = miner.downcast_arc::<crate::modules::miner::Miner>().ok();
212 *self.miner.lock().unwrap() = miner;
213 }
214
215 crate::matchers::register_link_matchers(self)?;
216
217 Ok(())
218 }
219
220 pub async fn handle_daemon_event(self: &Arc<Self>, event: DaemonEvent) -> Result<()> {
221 match event.kind() {
222 DaemonKind::Kaspad => {
223 let node = self.node.lock().unwrap().clone();
224 if let Some(node) = node {
225 node.handle_event(self, event.into()).await?;
226 } else {
227 panic!("Stdio handler: node module is not initialized");
228 }
229 }
230 DaemonKind::CpuMiner => {
231 let miner = self.miner.lock().unwrap().clone();
232 if let Some(miner) = miner {
233 miner.handle_event(self, event.into()).await?;
234 } else {
235 panic!("Stdio handler: miner module is not initialized");
236 }
237 }
238 }
239
240 Ok(())
241 }
242
243 pub async fn start(self: &Arc<Self>) -> Result<()> {
244 self.start_notification_pipe_task();
245 self.handlers.start(self).await?;
246 self.wallet.load_settings().await.unwrap_or_else(|_| log_error!("Unable to load settings, discarding..."));
248 self.wallet.start().await?;
249 Ok(())
250 }
251
252 pub async fn run(self: &Arc<Self>) -> Result<()> {
253 self.term().run().await?;
254 Ok(())
255 }
256
257 pub async fn stop(self: &Arc<Self>) -> Result<()> {
258 self.wallet.stop().await?;
259
260 self.handlers.stop(self).await?;
261
262 self.stop_notification_pipe_task().await?;
264 Ok(())
265 }
266
267 async fn stop_notification_pipe_task(self: &Arc<Self>) -> Result<()> {
268 self.notifications_task_ctl.signal(()).await?;
269 Ok(())
270 }
271
272 fn start_notification_pipe_task(self: &Arc<Self>) {
273 let this = self.clone();
274 let multiplexer = MultiplexerChannel::from(self.wallet.multiplexer());
275
276 workflow_core::task::spawn(async move {
277 loop {
278 select! {
279
280 _ = this.notifications_task_ctl.request.receiver.recv().fuse() => {
281 break;
282 },
283
284 msg = multiplexer.receiver.recv().fuse() => {
285
286 if let Ok(msg) = msg {
287 match *msg {
288 Events::WalletPing => {
289 },
291 Events::Metrics { network_id : _, metrics : _ } => {
292 }
294 Events::Error { message } => { terrorln!(this,"{message}"); },
295 Events::UtxoProcStart => {},
296 Events::UtxoProcStop => {},
297 Events::UtxoProcError { message } => {
298 terrorln!(this,"{message}");
299 },
300 #[allow(unused_variables)]
301 Events::Connect{ url, network_id } => {
302 },
304 #[allow(unused_variables)]
305 Events::Disconnect{ url, network_id } => {
306 tprintln!(this, "Disconnected from {}",url.unwrap_or("N/A".to_string()));
307 this.term().refresh_prompt();
308 },
309 Events::UtxoIndexNotEnabled { .. } => {
310 tprintln!(this, "Error: Kaspa node UTXO index is not enabled...")
311 },
312 Events::SyncState { sync_state } => {
313
314 if sync_state.is_synced() && this.wallet().is_open() {
315 let guard = this.wallet().guard();
316 let guard = guard.lock().await;
317 if let Err(error) = this.wallet().reload(false, &guard).await {
318 terrorln!(this, "Unable to reload wallet: {error}");
319 }
320 }
321
322 this.sync_state.lock().unwrap().replace(sync_state);
323 this.term().refresh_prompt();
324 }
325 Events::ServerStatus {
326 is_synced,
327 server_version,
328 url,
329 ..
330 } => {
331
332 tprintln!(this, "Connected to Kaspa node version {server_version} at {}", url.unwrap_or("N/A".to_string()));
333
334 let is_open = this.wallet.is_open();
335
336 if !is_synced {
337 if is_open {
338 terrorln!(this, "Unable to update the wallet state - Kaspa node is currently syncing with the network...");
339
340 } else {
341 terrorln!(this, "Kaspa node is currently syncing with the network, please wait for the sync to complete...");
342 }
343 }
344
345 this.term().refresh_prompt();
346
347 },
348 Events::WalletHint {
349 hint
350 } => {
351
352 if let Some(hint) = hint {
353 tprintln!(this, "\nYour wallet hint is: {hint}\n");
354 }
355
356 },
357 Events::AccountSelection { .. } => { },
358 Events::WalletCreate { .. } => { },
359 Events::WalletError { .. } => { },
360 Events::WalletOpen { .. } |
363 Events::WalletReload { .. } => { },
364 Events::WalletClose => {
365 this.term().refresh_prompt();
366 },
367 Events::PrvKeyDataCreate { .. } => { },
368 Events::AccountDeactivation { .. } => { },
369 Events::AccountActivation { .. } => {
370 this.list().await.unwrap_or_else(|err|terrorln!(this, "{err}"));
372
373 this.wallet().autoselect_default_account_if_single().await.ok();
375 this.term().refresh_prompt();
376 },
377 Events::AccountCreate { .. } => { },
378 Events::AccountUpdate { .. } => { },
379 Events::DaaScoreChange { current_daa_score } => {
380 if this.is_mutted() && this.flags.get(Track::Daa) {
381 tprintln!(this, "{NOTIFY} DAA: {current_daa_score}");
382 }
383 },
384 Events::Discovery { .. } => { }
385 Events::Reorg {
386 record
387 } => {
388 if !this.is_mutted() || (this.is_mutted() && this.flags.get(Track::Pending)) {
389 let guard = this.wallet.guard();
390 let guard = guard.lock().await;
391
392 let include_utxos = this.flags.get(Track::Utxo);
393 let tx = record.format_transaction_with_state(&this.wallet,Some("reorg"),include_utxos, &guard).await;
394 tx.iter().for_each(|line|tprintln!(this,"{NOTIFY} {line}"));
395 }
396 },
397 Events::Stasis {
398 record
399 } => {
400 if !this.is_mutted() || (this.is_mutted() && this.flags.get(Track::Pending)) {
402 let guard = this.wallet.guard();
403 let guard = guard.lock().await;
404
405 let include_utxos = this.flags.get(Track::Utxo);
406 let tx = record.format_transaction_with_state(&this.wallet,Some("stasis"),include_utxos, &guard).await;
407 tx.iter().for_each(|line|tprintln!(this,"{NOTIFY} {line}"));
408 }
409 },
410 Events::Pending {
420 record
421 } => {
422 if !this.is_mutted() || (this.is_mutted() && this.flags.get(Track::Pending)) {
423 let guard = this.wallet.guard();
424 let guard = guard.lock().await;
425
426 let include_utxos = this.flags.get(Track::Utxo);
427 let tx = record.format_transaction_with_state(&this.wallet,Some("pending"),include_utxos, &guard).await;
428 tx.iter().for_each(|line|tprintln!(this,"{NOTIFY} {line}"));
429 }
430 },
431 Events::Maturity {
432 record
433 } => {
434 if !this.is_mutted() || (this.is_mutted() && this.flags.get(Track::Tx)) {
435 let guard = this.wallet.guard();
436 let guard = guard.lock().await;
437
438 let include_utxos = this.flags.get(Track::Utxo);
439 let tx = record.format_transaction_with_state(&this.wallet,Some("confirmed"),include_utxos, &guard).await;
440 tx.iter().for_each(|line|tprintln!(this,"{NOTIFY} {line}"));
441 }
442 },
443 Events::Balance {
462 balance,
463 id,
464 } => {
465
466 if !this.is_mutted() || (this.is_mutted() && this.flags.get(Track::Balance)) {
467 let network_id = this.wallet.network_id().expect("missing network type");
468 let network_type = NetworkType::from(network_id);
469 let balance_strings = BalanceStrings::from((balance.as_ref(),&network_type, None));
470 let id = id.short();
471
472 let mature_utxo_count = balance.as_ref().map(|balance|balance.mature_utxo_count.separated_string()).unwrap_or("N/A".to_string());
473 let pending_utxo_count = balance.as_ref().map(|balance|balance.pending_utxo_count).unwrap_or(0);
474
475 let pending_utxo_info = if pending_utxo_count > 0 {
476 format!("({} pending)", pending_utxo_count)
477 } else { "".to_string() };
478 let utxo_info = style(format!("{mature_utxo_count} UTXOs {pending_utxo_info}")).dim();
479
480 tprintln!(this, "{NOTIFY} {} {id}: {balance_strings} {utxo_info}",style("balance".pad_to_width(8)).blue());
481 }
482
483 this.term().refresh_prompt();
484 }
485 }
486 }
487 }
488 }
489 }
490
491 this.notifications_task_ctl
492 .response
493 .sender
494 .send(())
495 .await
496 .unwrap_or_else(|err| log_error!("WalletCli::notification_pipe_task() unable to signal task shutdown: `{err}`"));
497 });
498 }
499
500 pub(crate) async fn ask_wallet_secret(&self, account: Option<&Arc<dyn Account>>) -> Result<(Secret, Option<Secret>)> {
505 let wallet_secret = Secret::new(self.term().ask(true, "Enter wallet password: ").await?.trim().as_bytes().to_vec());
506
507 let payment_secret = if let Some(account) = account {
508 if self.wallet().is_account_key_encrypted(account).await?.is_some_and(|f| f) {
509 Some(Secret::new(self.term().ask(true, "Enter payment password: ").await?.trim().as_bytes().to_vec()))
510 } else {
511 None
512 }
513 } else {
514 None
515 };
516
517 Ok((wallet_secret, payment_secret))
518 }
519
520 pub async fn account(&self) -> Result<Arc<dyn Account>> {
521 if let Ok(account) = self.wallet.account() {
522 Ok(account)
523 } else {
524 let account = self.select_account().await?;
525 self.wallet.select(Some(&account)).await?;
526 Ok(account)
527 }
528 }
529
530 pub async fn find_accounts_by_name_or_id(&self, pat: &str) -> Result<Arc<dyn Account>> {
531 let matches = self.wallet().find_accounts_by_name_or_id(pat).await?;
532 if matches.is_empty() {
533 Err(Error::AccountNotFound(pat.to_string()))
534 } else if matches.len() > 1 {
535 Err(Error::AmbiguousAccount(pat.to_string()))
536 } else {
537 Ok(matches[0].clone())
538 }
539 }
540
541 pub async fn prompt_account(&self) -> Result<Arc<dyn Account>> {
542 self.select_account_with_args(false).await
543 }
544
545 pub async fn select_account(&self) -> Result<Arc<dyn Account>> {
546 self.select_account_with_args(true).await
547 }
548
549 async fn select_account_with_args(&self, autoselect: bool) -> Result<Arc<dyn Account>> {
550 let guard = self.wallet.guard();
551 let guard = guard.lock().await;
552
553 let mut selection = None;
554
555 let mut list_by_key = Vec::<(Arc<PrvKeyDataInfo>, Vec<(usize, Arc<dyn Account>)>)>::new();
556 let mut flat_list = Vec::<Arc<dyn Account>>::new();
557
558 let mut keys = self.wallet.keys().await?;
559 while let Some(key) = keys.try_next().await? {
560 let mut prv_key_accounts = Vec::new();
561 let mut accounts = self.wallet.accounts(Some(key.id), &guard).await?;
562 while let Some(account) = accounts.next().await {
563 let account = account?;
564 prv_key_accounts.push((flat_list.len(), account.clone()));
565 flat_list.push(account.clone());
566 }
567
568 list_by_key.push((key.clone(), prv_key_accounts));
569 }
570
571 let mut watch_accounts = Vec::<(usize, Arc<dyn Account>)>::new();
572 let mut unfiltered_accounts = self.wallet.accounts(None, &guard).await?;
573
574 while let Some(account) = unfiltered_accounts.try_next().await? {
575 if account.feature().is_some() {
576 watch_accounts.push((flat_list.len(), account.clone()));
577 flat_list.push(account.clone());
578 }
579 }
580
581 if flat_list.is_empty() {
582 return Err(Error::NoAccounts);
583 } else if autoselect && flat_list.len() == 1 {
584 return Ok(flat_list.pop().unwrap());
585 }
586
587 while selection.is_none() {
588 tprintln!(self);
589
590 list_by_key.iter().for_each(|(prv_key_data_info, accounts)| {
591 tprintln!(self, "• {prv_key_data_info}");
592
593 accounts.iter().for_each(|(seq, account)| {
594 let seq = style(seq.to_string()).cyan();
595 let ls_string = account.get_list_string().unwrap_or_else(|err| panic!("{err}"));
596 tprintln!(self, " {seq}: {ls_string}");
597 })
598 });
599
600 if !watch_accounts.is_empty() {
601 tprintln!(self, "• watch-only");
602 }
603
604 watch_accounts.iter().for_each(|(seq, account)| {
605 let seq = style(seq.to_string()).cyan();
606 let ls_string = account.get_list_string().unwrap_or_else(|err| panic!("{err}"));
607 tprintln!(self, " {seq}: {ls_string}");
608 });
609
610 tprintln!(self);
611
612 let range = if flat_list.len() > 1 { format!("[{}..{}] ", 0, flat_list.len() - 1) } else { "".to_string() };
613
614 let text =
615 self.term().ask(false, &format!("Please select account {}or <enter> to abort: ", range)).await?.trim().to_string();
616 if text.is_empty() {
617 return Err(Error::UserAbort);
618 } else {
619 match text.parse::<usize>() {
620 Ok(seq) if seq < flat_list.len() => selection = flat_list.get(seq).cloned(),
621 _ => {}
622 };
623 }
624 }
625
626 let account = selection.unwrap();
627 let ident = style(account.name_with_id()).blue();
628 tprintln!(self, "selecting account: {ident}");
629
630 Ok(account)
631 }
632
633 pub async fn select_private_key(&self) -> Result<Arc<PrvKeyDataInfo>> {
634 self.select_private_key_with_args(true).await
635 }
636
637 pub async fn select_private_key_with_args(&self, autoselect: bool) -> Result<Arc<PrvKeyDataInfo>> {
638 let mut selection = None;
639
640 let mut flat_list = Vec::<Arc<PrvKeyDataInfo>>::new();
642
643 let mut keys = self.wallet.keys().await?;
644 while let Some(key) = keys.try_next().await? {
645 flat_list.push(key);
646 }
647
648 if flat_list.is_empty() {
649 return Err(Error::NoKeys);
650 } else if autoselect && flat_list.len() == 1 {
651 return Ok(flat_list.pop().unwrap());
652 }
653
654 while selection.is_none() {
655 tprintln!(self);
656
657 flat_list.iter().enumerate().for_each(|(seq, prv_key_data_info)| {
658 tprintln!(self, " {seq}: {prv_key_data_info}");
659 });
660
661 tprintln!(self);
662
663 let range = if flat_list.len() > 1 { format!("[{}..{}] ", 0, flat_list.len() - 1) } else { "".to_string() };
664
665 let text =
666 self.term().ask(false, &format!("Please select private key {}or <enter> to abort: ", range)).await?.trim().to_string();
667 if text.is_empty() {
668 return Err(Error::UserAbort);
669 } else {
670 match text.parse::<usize>() {
671 Ok(seq) if seq < flat_list.len() => selection = flat_list.get(seq).cloned(),
672 _ => {}
673 };
674 }
675 }
676
677 let prv_key_data_info = selection.unwrap();
678 tprintln!(self, "\nselecting private key: {prv_key_data_info}\n");
679
680 Ok(prv_key_data_info)
681 }
682
683 pub async fn list(&self) -> Result<()> {
684 let guard = self.wallet.guard();
685 let guard = guard.lock().await;
686
687 let mut keys = self.wallet.keys().await?;
688
689 tprintln!(self);
690 while let Some(key) = keys.try_next().await? {
691 tprintln!(self, "• {}", style(&key).dim());
692
693 let mut accounts = self.wallet.accounts(Some(key.id), &guard).await?;
694 while let Some(account) = accounts.try_next().await? {
695 let receive_address = account.receive_address()?;
696 tprintln!(self, " • {}", account.get_list_string()?);
697 tprintln!(self, " {}", style(receive_address.to_string()).blue());
698 }
699 }
700
701 let mut unfiltered_accounts = self.wallet.accounts(None, &guard).await?;
702 let mut feature_header_printed = false;
703 while let Some(account) = unfiltered_accounts.try_next().await? {
704 if let Some(feature) = account.feature() {
705 if !feature_header_printed {
706 tprintln!(self, "{}", style("• watch-only").dim());
707 feature_header_printed = true;
708 }
709 tprintln!(self, " • {}", account.get_list_string().unwrap());
710 tprintln!(self, " • {}", style(feature).cyan());
711 }
712 }
713 tprintln!(self);
714
715 Ok(())
716 }
717
718 pub async fn shutdown(&self) -> Result<()> {
719 if !self.shutdown.load(Ordering::SeqCst) {
720 self.shutdown.store(true, Ordering::SeqCst);
721
722 tprintln!(self, "{}", style("shutting down...").magenta());
723
724 let miner = self.daemons().try_cpu_miner();
725 let kaspad = self.daemons().try_kaspad();
726
727 if let Some(miner) = miner.as_ref() {
728 miner.mute(false).await?;
729 miner.stop().await?;
730 }
731
732 if let Some(kaspad) = kaspad.as_ref() {
733 kaspad.mute(false).await?;
734 kaspad.stop().await?;
735 }
736
737 if let Some(miner) = miner.as_ref() {
738 miner.join().await?;
739 }
740
741 if let Some(kaspad) = kaspad.as_ref() {
742 kaspad.join().await?;
743 }
744
745 self.term().exit().await;
746 }
747
748 Ok(())
749 }
750
751 fn sync_state(&self) -> Option<String> {
752 if let Some(state) = self.sync_state.lock().unwrap().as_ref() {
753 match state {
754 SyncState::Proof { level } => {
755 if *level == 0 {
756 Some([style("SYNC").red().to_string(), style("...").black().to_string()].join(" "))
757 } else {
758 Some([style("SYNC PROOF").red().to_string(), style(level.separated_string()).dim().to_string()].join(" "))
759 }
760 }
761 SyncState::Headers { headers, progress } => Some(
762 [
763 style("SYNC IBD HDRS").red().to_string(),
764 style(format!("{} ({}%)", headers.separated_string(), progress)).dim().to_string(),
765 ]
766 .join(" "),
767 ),
768 SyncState::Blocks { blocks, progress } => Some(
769 [
770 style("SYNC IBD BLOCKS").red().to_string(),
771 style(format!("{} ({}%)", blocks.separated_string(), progress)).dim().to_string(),
772 ]
773 .join(" "),
774 ),
775 SyncState::TrustSync { processed, total } => {
776 let progress = processed * 100 / total;
777 Some(
778 [
779 style("SYNC TRUST").red().to_string(),
780 style(format!("{} ({}%)", processed.separated_string(), progress)).dim().to_string(),
781 ]
782 .join(" "),
783 )
784 }
785 SyncState::UtxoSync { total, .. } => {
786 Some([style("SYNC UTXO").red().to_string(), style(total.separated_string()).dim().to_string()].join(" "))
787 }
788 SyncState::UtxoResync => Some([style("SYNC").red().to_string(), style("UTXO").black().to_string()].join(" ")),
789 SyncState::NotSynced => Some([style("SYNC").red().to_string(), style("...").black().to_string()].join(" ")),
790 SyncState::Synced { .. } => None,
791 }
792 } else {
793 Some(style("SYNC").red().to_string())
794 }
795 }
796}
797
798#[async_trait]
799impl Cli for KaspaCli {
800 fn init(self: Arc<Self>, term: &Arc<Terminal>) -> TerminalResult<()> {
801 *self.term.lock().unwrap() = Some(term.clone());
802
803 self.notifier().try_init()?;
804
805 term.register_event_handler(Arc::new(Box::new(move |event| match event {
806 TerminalEvent::Copy | TerminalEvent::Paste => {
807 self.notifier().notify(Notification::Clipboard);
808 }
809 })))?;
810
811 Ok(())
812 }
813
814 async fn digest(self: Arc<Self>, term: Arc<Terminal>, cmd: String) -> TerminalResult<()> {
815 *self.last_interaction.lock().unwrap() = Instant::now();
816 if let Err(err) = self.handlers.execute(&self, &cmd).await {
817 term.writeln(style(err.to_string()).red().to_string());
818 }
819 Ok(())
820 }
821
822 async fn complete(self: Arc<Self>, _term: Arc<Terminal>, cmd: String) -> TerminalResult<Option<Vec<String>>> {
823 let list = self.handlers.complete(&self, &cmd).await?;
824 Ok(list)
825 }
826
827 fn prompt(&self) -> Option<String> {
828 if self.shutdown.load(Ordering::SeqCst) {
829 return Some("halt $ ".to_string());
830 }
831
832 let mut prompt = vec![];
833
834 let node_running = if let Some(node) = self.node.lock().unwrap().as_ref() { node.is_running() } else { false };
835
836 let _miner_running = if let Some(miner) = self.miner.lock().unwrap().as_ref() { miner.is_running() } else { false };
837
838 if (self.wallet.is_open() && !self.wallet.is_connected()) || (node_running && !self.wallet.is_connected()) {
846 prompt.push(style("N/C").red().to_string());
847 } else if self.wallet.is_connected() && !self.wallet.is_synced() {
848 if let Some(state) = self.sync_state() {
849 prompt.push(state);
850 }
851 }
852
853 if let Some(descriptor) = self.wallet.descriptor() {
854 let title = descriptor.title.unwrap_or(descriptor.filename);
855 if title.to_lowercase().as_str() != "kaspa" {
856 prompt.push(title);
857 }
858
859 if let Ok(account) = self.wallet.account() {
860 prompt.push(style(account.name_with_id()).blue().to_string());
861
862 if let Ok(balance) = account.balance_as_strings(None) {
863 if let Some(pending) = balance.pending {
864 prompt.push(format!("{} ({})", balance.mature, pending));
865 } else {
866 prompt.push(balance.mature);
867 }
868 } else {
869 prompt.push("N/A".to_string());
870 }
871 }
872 }
873
874 prompt.is_not_empty().then(|| prompt.join(" • ") + " $ ")
875 }
876}
877
878impl cli::Context for KaspaCli {
879 fn term(&self) -> Arc<Terminal> {
880 self.term.lock().unwrap().as_ref().unwrap().clone()
881 }
882}
883
884impl KaspaCli {}
885
886#[allow(dead_code)]
887async fn select_item<T>(
888 term: &Arc<Terminal>,
889 prompt: &str,
890 argv: &mut Vec<String>,
891 iter: impl Stream<Item = Result<Arc<T>>>,
892) -> Result<Arc<T>>
893where
894 T: std::fmt::Display + IdT + Clone + Send + Sync + 'static,
895{
896 let mut selection = None;
897 let list = iter.try_collect::<Vec<_>>().await?;
898
899 if !argv.is_empty() {
900 let text = argv.remove(0);
901 let matched = list
902 .into_iter()
903 .filter(|item| item.id().to_hex().starts_with(&text))
905 .collect::<Vec<_>>();
906
907 if matched.len() == 1 {
908 return Ok(matched.first().cloned().unwrap());
909 } else {
910 return Err(Error::MultipleMatches(text));
911 }
912 }
913
914 while selection.is_none() {
915 list.iter().enumerate().for_each(|(seq, item)| {
916 term.writeln(format!("{}: {} ({})", seq, item, item.id().to_hex()));
917 });
918
919 let text = term.ask(false, &format!("{prompt} ({}..{}) or <enter> to abort: ", 0, list.len() - 1)).await?.trim().to_string();
920 if text.is_empty() {
921 term.writeln("aborting...");
922 return Err(Error::UserAbort);
923 } else {
924 match text.parse::<usize>() {
925 Ok(seq) if seq < list.len() => selection = list.get(seq).cloned(),
926 _ => {}
927 };
928 }
929 }
930
931 Ok(selection.unwrap())
932}
933
934pub async fn kaspa_cli(terminal_options: TerminalOptions, banner: Option<String>) -> Result<()> {
974 KaspaCli::init();
975
976 let options = Options::new(terminal_options, None);
977 let cli = KaspaCli::try_new_arc(options).await?;
978
979 let banner =
980 banner.unwrap_or_else(|| format!("Kaspa Cli Wallet v{} (type 'help' for list of commands)", env!("CARGO_PKG_VERSION")));
981 cli.term().writeln(banner);
982
983 #[cfg(not(target_arch = "wasm32"))]
985 workflow_log::pipe(Some(cli.clone()));
986
987 cli.register_handlers()?;
988
989 cli.start().await?;
991
992 cli.run().await?;
994
995 cli.stop().await?;
997
998 Ok(())
999}
1000
1001mod panic_handler {
1002 use regex::Regex;
1003 use wasm_bindgen::prelude::*;
1004
1005 #[wasm_bindgen]
1006 extern "C" {
1007 #[wasm_bindgen(js_namespace = console, js_name="error")]
1008 pub fn console_error(msg: String);
1009
1010 type Error;
1011
1012 #[wasm_bindgen(constructor)]
1013 fn new() -> Error;
1014
1015 #[wasm_bindgen(structural, method, getter)]
1016 fn stack(error: &Error) -> String;
1017 }
1018
1019 pub fn process(info: &std::panic::PanicInfo) -> String {
1020 let mut msg = info.to_string();
1021
1022 msg.push_str("\n\nStack:\n\n");
1034 let e = Error::new();
1035 let stack = e.stack();
1036
1037 let regex = Regex::new(r"chrome-extension://[^/]+").unwrap();
1038 let stack = regex.replace_all(&stack, "");
1039
1040 msg.push_str(&stack);
1041
1042 msg.push_str("\n\n");
1048
1049 msg
1050 }
1051}
1052
1053impl KaspaCli {
1054 pub fn init_panic_hook(self: &Arc<Self>) {
1055 let this = self.clone();
1056 let handler = move |info: &std::panic::PanicInfo| {
1057 let msg = panic_handler::process(info);
1058 this.term().writeln(msg.crlf());
1059 panic_handler::console_error(msg);
1060 };
1061
1062 std::panic::set_hook(Box::new(handler));
1063
1064 workflow_log::pipe(Some(self.clone()));
1066 }
1067}