From 603ebe74f82773e1538ffa0a28138897ce112377 Mon Sep 17 00:00:00 2001 From: skoupidi Date: Thu, 12 Jun 2025 16:45:03 +0300 Subject: [PATCH] drk: introduced interactive shell infra --- Cargo.lock | 11 ++ bin/drk/Cargo.toml | 1 + bin/drk/drk_config.toml | 9 ++ bin/drk/src/cli_util.rs | 4 + bin/drk/src/interactive.rs | 152 ++++++++++++++++++ bin/drk/src/lib.rs | 3 + bin/drk/src/main.rs | 22 +++ contrib/localnet/darkfid-single-node/drk.toml | 3 + 8 files changed, 205 insertions(+) create mode 100644 bin/drk/src/interactive.rs diff --git a/Cargo.lock b/Cargo.lock index 76ba700d7..1fd3fdd92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2468,6 +2468,7 @@ dependencies = [ "darkfi_money_contract", "easy-parallel", "lazy_static", + "linenoise-rs", "log", "num-bigint", "prettytable-rs", @@ -4098,6 +4099,16 @@ dependencies = [ "url", ] +[[package]] +name = "linenoise-rs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e207483a55cd856bc85af576f663b2ebd25616e7b624c4ba42cff9ec2f0631" +dependencies = [ + "lazy_static", + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.9.4" diff --git a/bin/drk/Cargo.toml b/bin/drk/Cargo.toml index 4e7ef9212..7a8da072d 100644 --- a/bin/drk/Cargo.toml +++ b/bin/drk/Cargo.toml @@ -21,6 +21,7 @@ darkfi-serial = "0.5.0" blake3 = "1.8.2" bs58 = "0.5.1" lazy_static = "1.5.0" +linenoise-rs = "0.1.1" log = "0.4.27" num-bigint = "0.4.6" prettytable-rs = "0.10.0" diff --git a/bin/drk/drk_config.toml b/bin/drk/drk_config.toml index 04ae2ea33..130acb2fa 100644 --- a/bin/drk/drk_config.toml +++ b/bin/drk/drk_config.toml @@ -26,6 +26,9 @@ wallet_pass = "changeme" # darkfid JSON-RPC endpoint endpoint = "tcp://127.0.0.1:8240" +# Path to interactive shell history file +history_path = "~/.local/share/darkfi/drk/localnet/history.txt" + # Testnet blockchain network configuration [network_config."testnet"] # Path to blockchain cache database @@ -40,6 +43,9 @@ wallet_pass = "changeme" # darkfid JSON-RPC endpoint endpoint = "tcp://127.0.0.1:8340" +# Path to interactive shell history file +history_path = "~/.local/share/darkfi/drk/testnet/history.txt" + # Mainnet blockchain network configuration [network_config."mainnet"] # Path to blockchain cache database @@ -53,3 +59,6 @@ wallet_pass = "changeme" # darkfid JSON-RPC endpoint endpoint = "tcp://127.0.0.1:8440" + +# Path to interactive shell history file +history_path = "~/.local/share/darkfi/drk/mainnet/history.txt" diff --git a/bin/drk/src/cli_util.rs b/bin/drk/src/cli_util.rs index a11f9443a..6a834306e 100644 --- a/bin/drk/src/cli_util.rs +++ b/bin/drk/src/cli_util.rs @@ -113,6 +113,9 @@ pub async fn kaching() { pub fn generate_completions(shell: &str) -> Result<()> { // Sub-commands + // Interactive + let interactive = SubCommand::with_name("interactive").about("Enter Drk interactive shell"); + // Kaching let kaching = SubCommand::with_name("kaching").about("Fun"); @@ -504,6 +507,7 @@ pub fn generate_completions(shell: &str) -> Result<()> { .help("Blockchain network to use"); let command = vec![ + interactive, kaching, ping, completions, diff --git a/bin/drk/src/interactive.rs b/bin/drk/src/interactive.rs new file mode 100644 index 000000000..c0ad966bb --- /dev/null +++ b/bin/drk/src/interactive.rs @@ -0,0 +1,152 @@ +/* This file is part of DarkFi (https://dark.fi) + * + * Copyright (C) 2020-2025 Dyne.org foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +use linenoise_rs::{ + linenoise, linenoise_history_add, linenoise_history_load, linenoise_history_save, + linenoise_set_completion_callback, linenoise_set_hints_callback, +}; + +use darkfi::{system::StoppableTask, util::path::expand_path}; + +use crate::{ + cli_util::{generate_completions, kaching}, + Drk, +}; + +// TODO: +// 1. add rest commands handling, along with their completions and hints. +// 2. add input definitions, so you input from files not just stdin. +// 3. add output definitions, so you can output to files not just stdout. +// 4. create a transactions cache in the wallet db, so you can use it to handle them. + +/// Auxiliary function to define the interactive shell completions. +fn completion(buf: &str, lc: &mut Vec) { + if buf.starts_with("h") { + lc.push("help".to_string()); + return + } + + if buf.starts_with("k") { + lc.push("kaching".to_string()); + return + } + + if buf.starts_with("p") { + lc.push("ping".to_string()); + return + } + + if buf.starts_with("c") { + lc.push("completions".to_string()); + } +} + +/// Auxiliary function to define the interactive shell hints. +fn hints(buf: &str) -> Option<(String, i32, bool)> { + match buf { + "completions " => Some(("{shell}".to_string(), 35, false)), // 35 = magenta + _ => None, + } +} + +/// Auxiliary function to start provided Drk as an interactive shell. +pub async fn interactive(drk: &Drk, history_path: &str) { + // Expand the history file path + let history_path = match expand_path(history_path) { + Ok(p) => p, + Err(e) => { + println!("Error while expanding history file path: {e}"); + return + } + }; + let history_path = history_path.into_os_string(); + let history_file = history_path.to_str().unwrap(); + + // Set the completion callback. This will be called every time the + // user uses the key. + linenoise_set_completion_callback(completion); + + // Set the shell hints + linenoise_set_hints_callback(hints); + + // Load history from file.The history file is just a plain text file + // where entries are separated by newlines. + let _ = linenoise_history_load(history_file); + + // Create a detached task to use for block subscription + let subscription_active = false; + let subscription_task = StoppableTask::new(); + + // Start the interactive shell + loop { + // Grab input or end if Ctrl-D or Ctrl-C was pressed + let Some(line) = linenoise("drk> ") else { + // Stop the subscription task if its active + if subscription_active { + subscription_task.stop().await; + } + + // Write history file + let _ = linenoise_history_save(history_file); + + return + }; + + // Check if line is empty + if line.is_empty() { + continue + } + + // Add line to history + linenoise_history_add(&line); + + // Parse command parts + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.is_empty() { + continue + } + + // Handle command + match parts[0] { + "help" => println!("hhhheeeelp"), + "kaching" => kaching().await, + "ping" => handle_ping(drk).await, + "completions" => handle_completions(&parts), + _ => println!("Unreconized command: {}", parts[0]), + } + } +} + +/// Auxiliary function to define the ping command handling. +async fn handle_ping(drk: &Drk) { + if let Err(e) = drk.ping().await { + println!("Error while executing ping command: {e}") + } +} + +/// Auxiliary function to define the completions command handling. +fn handle_completions(parts: &[&str]) { + if parts.len() != 2 { + println!("Malformed `completions` command"); + println!("Usage: completions {{shell}}"); + } + + if let Err(e) = generate_completions(parts[1]) { + println!("Error while executing completions command: {e}") + } +} diff --git a/bin/drk/src/lib.rs b/bin/drk/src/lib.rs index cb4b51ad1..546c55206 100644 --- a/bin/drk/src/lib.rs +++ b/bin/drk/src/lib.rs @@ -41,6 +41,9 @@ pub mod token; /// CLI utility functions pub mod cli_util; +/// Drk interactive shell +pub mod interactive; + /// Wallet functionality related to Money pub mod money; diff --git a/bin/drk/src/main.rs b/bin/drk/src/main.rs index 93c076b06..f366688c2 100644 --- a/bin/drk/src/main.rs +++ b/bin/drk/src/main.rs @@ -57,6 +57,7 @@ use drk::{ generate_completions, kaching, parse_token_pair, parse_tx_from_stdin, parse_value_pair, }, dao::{DaoParams, ProposalRecord}, + interactive::interactive, money::BALANCE_BASE10_DECIMALS, swap::PartialSwapData, Drk, @@ -100,6 +101,9 @@ struct Args { // don't forget to update cli_util::generate_completions() #[derive(Clone, Debug, Deserialize, StructOpt)] enum Subcmd { + /// Enter Drk interactive shell + Interactive, + /// Fun Kaching, @@ -565,6 +569,10 @@ struct BlockchainNetwork { #[structopt(short, long, default_value = "tcp://127.0.0.1:8240")] /// darkfid JSON-RPC endpoint endpoint: Url, + + #[structopt(long, default_value = "~/.local/share/darkfi/drk/localnet/history.txt")] + /// Path to interactive shell history file + history_path: String, } /// Auxiliary function to parse darkfid configuration file and extract requested @@ -648,6 +656,20 @@ async fn realmain(args: Args, ex: Arc>) -> Result<()> { }; match args.command { + Subcmd::Interactive => { + let drk = new_wallet( + blockchain_config.cache_path, + blockchain_config.wallet_path, + blockchain_config.wallet_pass, + Some(blockchain_config.endpoint), + ex, + args.fun, + ) + .await; + interactive(&drk, &blockchain_config.history_path).await; + drk.stop_rpc_client().await + } + Subcmd::Kaching => { if !args.fun { println!("Apparently you don't like fun..."); diff --git a/contrib/localnet/darkfid-single-node/drk.toml b/contrib/localnet/darkfid-single-node/drk.toml index 1ded5ad8a..8215c801b 100644 --- a/contrib/localnet/darkfid-single-node/drk.toml +++ b/contrib/localnet/darkfid-single-node/drk.toml @@ -22,3 +22,6 @@ wallet_pass = "testing" # darkfid JSON-RPC endpoint endpoint = "tcp://127.0.0.1:48240" + +# Path to interactive shell history file +history_path = "drk/history.txt"