kaspa_cli_lib/
cli.rs

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        // wallet starts rpc and notifier
247        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        // stop notification pipe task
263        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                                    // log_info!("Kaspa NG - received wallet ping");
290                                },
291                                Events::Metrics { network_id : _, metrics : _ } => {
292                                    // log_info!("Kaspa NG - received metrics event {metrics:?}")
293                                }
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                                    // log_info!("Connected to {url}");
303                                },
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::WalletReady { .. } => { },
361
362                                Events::WalletOpen { .. } |
363                                Events::WalletReload { .. } => { },
364                                Events::WalletClose => {
365                                    this.term().refresh_prompt();
366                                },
367                                Events::PrvKeyDataCreate { .. } => { },
368                                Events::AccountDeactivation { .. } => { },
369                                Events::AccountActivation { .. } => {
370                                    // list all accounts
371                                    this.list().await.unwrap_or_else(|err|terrorln!(this, "{err}"));
372
373                                    // load default account if only one account exists
374                                    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                                    // Pending and coinbase stasis fall under the same `Track` category
401                                    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::External {
411                                //     record
412                                // } => {
413                                //     if !this.is_mutted() || (this.is_mutted() && this.flags.get(Track::Tx)) {
414                                //         let include_utxos = this.flags.get(Track::Utxo);
415                                //         let tx = record.format_with_state(&this.wallet,Some("external"),include_utxos).await;
416                                //         tx.iter().for_each(|line|tprintln!(this,"{NOTIFY} {line}"));
417                                //     }
418                                // },
419                                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::Outgoing {
444                                //     record
445                                // } => {
446                                //     if !this.is_mutted() || (this.is_mutted() && this.flags.get(Track::Tx)) {
447                                //         let include_utxos = this.flags.get(Track::Utxo);
448                                //         let tx = record.format_with_state(&this.wallet,Some("confirmed"),include_utxos).await;
449                                //         tx.iter().for_each(|line|tprintln!(this,"{NOTIFY} {line}"));
450                                //     }
451                                // },
452                                // Events::Change {
453                                //     record
454                                // } => {
455                                //     if !this.is_mutted() || (this.is_mutted() && this.flags.get(Track::Tx)) {
456                                //         let include_utxos = this.flags.get(Track::Utxo);
457                                //         let tx = record.format_with_state(&this.wallet,Some("change"),include_utxos).await;
458                                //         tx.iter().for_each(|line|tprintln!(this,"{NOTIFY} {line}"));
459                                //     }
460                                // },
461                                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    // ---
501
502    /// Asks uses for a wallet secret, checks the supplied account's private key info
503    /// and if it requires a payment secret, asks for it as well.
504    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 list_by_key = Vec::<(Arc<PrvKeyDataInfo>, Vec<(usize, Arc<dyn Account>)>)>::new();
641        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        // match (node_running, miner_running) {
839        //     (true, true) => prompt.push(style("NM").green().to_string()),
840        //     (true, false) => prompt.push(style("N").green().to_string()),
841        //     (false, true) => prompt.push(style("M").green().to_string()),
842        //     _ => {}
843        // }
844
845        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            // - TODO match by name
904            .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
934// async fn select_variant<T>(term: &Arc<Terminal>, prompt: &str, argv: &mut Vec<String>) -> Result<T>
935// where
936//     T: ToString + DeserializeOwned + Clone + Serialize,
937// {
938//     if !argv.is_empty() {
939//         let text = argv.remove(0);
940//         if let Ok(v) = serde_json::from_str::<T>(text.as_str()) {
941//             return Ok(v);
942//         } else {
943//             let accepted = T::list().iter().map(|v| serde_json::to_string(v).unwrap()).collect::<Vec<_>>().join(", ");
944//             return Err(Error::UnrecognizedArgument(text, accepted));
945//         }
946//     }
947
948//     let mut selection = None;
949//     let list = T::list();
950//     while selection.is_none() {
951//         list.iter().enumerate().for_each(|(seq, item)| {
952//             let name = serde_json::to_string(item).unwrap();
953//             term.writeln(format!("{}: '{name}' - {}", seq, item.descr()));
954//         });
955
956//         let text = term.ask(false, &format!("{prompt} ({}..{}) or <enter> to abort: ", 0, list.len() - 1)).await?.trim().to_string();
957//         if text.is_empty() {
958//             term.writeln("aborting...");
959//             return Err(Error::UserAbort);
960//         } else if let Ok(v) = serde_json::from_str::<T>(text.as_str()) {
961//             selection = Some(v);
962//         } else {
963//             match text.parse::<usize>() {
964//                 Ok(seq) if seq > 0 && seq < list.len() => selection = list.get(seq).cloned(),
965//                 _ => {}
966//             };
967//         }
968//     }
969
970//     Ok(selection.unwrap())
971// }
972
973pub 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    // redirect the global log output to terminal
984    #[cfg(not(target_arch = "wasm32"))]
985    workflow_log::pipe(Some(cli.clone()));
986
987    cli.register_handlers()?;
988
989    // cli starts notification->term trace pipe task
990    cli.start().await?;
991
992    // terminal blocks async execution, delivering commands to the terminals
993    cli.run().await?;
994
995    // cli stops notification->term trace pipe task
996    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        // Add the error stack to our message.
1023        //
1024        // This ensures that even if the `console` implementation doesn't
1025        // include stacks for `console.error`, the stack is still available
1026        // for the user. Additionally, Firefox's console tries to clean up
1027        // stack traces, and ruins Rust symbols in the process
1028        // (https://bugzilla.mozilla.org/show_bug.cgi?id=1519569) but since
1029        // it only touches the logged message's associated stack, and not
1030        // the message's contents, by including the stack in the message
1031        // contents we make sure it is available to the user.
1032
1033        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        // Safari's devtools, on the other hand, _do_ mess with logged
1043        // messages' contents, so we attempt to break their heuristics for
1044        // doing that by appending some whitespace.
1045        // https://github.com/rustwasm/console_error_panic_hook/issues/7
1046
1047        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        // #[cfg(target_arch = "wasm32")]
1065        workflow_log::pipe(Some(self.clone()));
1066    }
1067}