From 668220d47f41122d35d71cbae79181cc0c88b103 Mon Sep 17 00:00:00 2001 From: skoupidi Date: Tue, 17 Jun 2025 15:53:01 +0300 Subject: [PATCH] drk/interactive: wallet command handling added --- bin/drk/src/interactive.rs | 315 ++++++++++++++++++++++++++++++++++++- bin/drk/src/main.rs | 4 +- 2 files changed, 314 insertions(+), 5 deletions(-) diff --git a/bin/drk/src/interactive.rs b/bin/drk/src/interactive.rs index 304a9f3b4..ce32540c5 100644 --- a/bin/drk/src/interactive.rs +++ b/bin/drk/src/interactive.rs @@ -16,7 +16,10 @@ * along with this program. If not, see . */ -use std::{io::ErrorKind, str::FromStr}; +use std::{ + io::{stdin, ErrorKind}, + str::FromStr, +}; use futures::{select, FutureExt}; use libc::{fcntl, F_GETFL, F_SETFL, O_NONBLOCK}; @@ -24,18 +27,23 @@ use linenoise_rs::{ linenoise_history_add, linenoise_history_load, linenoise_history_save, linenoise_set_completion_callback, linenoise_set_hints_callback, LinenoiseState, }; +use prettytable::{format, row, Table}; use smol::channel::{unbounded, Receiver, Sender}; use url::Url; use darkfi::{ cli_desc, system::{msleep, ExecutorPtr, StoppableTask, StoppableTaskPtr}, - util::path::expand_path, + util::{parse::encode_base10, path::expand_path}, + zk::halo2::Field, Error, }; +use darkfi_sdk::{crypto::FuncId, pasta::pallas}; +use darkfi_serial::{deserialize_async, serialize_async}; use crate::{ cli_util::{generate_completions, kaching}, + money::BALANCE_BASE10_DECIMALS, rpc::subscribe_blocks, DrkPtr, }; @@ -54,6 +62,7 @@ fn help() { println!("\tkaching: Fun"); println!("\tping: Send a ping request to the darkfid RPC endpoint"); println!("\tcompletions: Generate a SHELL completion script and print to stdout"); + println!("\twallet: Wallet operations"); println!( "\tsubscribe: Perform a scan and then subscribe to darkfid to listen for incoming blocks" ); @@ -86,6 +95,21 @@ fn completion(buf: &str, lc: &mut Vec) { return } + if buf.starts_with("w") { + lc.push("wallet".to_string()); + lc.push("wallet --initialize".to_string()); + lc.push("wallet --keygen".to_string()); + lc.push("wallet --balance".to_string()); + lc.push("wallet --address".to_string()); + lc.push("wallet --addresses".to_string()); + lc.push("wallet --default_address".to_string()); + lc.push("wallet --secrets".to_string()); + lc.push("wallet --import_secrets".to_string()); + lc.push("wallet --tree".to_string()); + lc.push("wallet --coins".to_string()); + return + } + if buf.starts_with("su") { lc.push("subscribe".to_string()); return @@ -108,6 +132,7 @@ fn completion(buf: &str, lc: &mut Vec) { if buf.starts_with("sc") { lc.push("scan".to_string()); + lc.push("scan --reset".to_string()); return } @@ -116,6 +141,7 @@ fn completion(buf: &str, lc: &mut Vec) { lc.push("subscribe".to_string()); lc.push("snooze".to_string()); lc.push("scan".to_string()); + lc.push("scan --reset".to_string()); return } @@ -129,7 +155,11 @@ fn completion(buf: &str, lc: &mut Vec) { fn hints(buf: &str) -> Option<(String, i32, bool)> { match buf { "completions " => Some(("{shell}".to_string(), 35, false)), // 35 = magenta - "scan " => Some(("--reset {height}".to_string(), 35, false)), // 35 = magenta + "wallet " => Some(("--{initialize|keygen|balance|address|addresses|default_address|secrets|import_secrets|tree|coins}".to_string(), 35, false)), + "wallet -" => Some(("-{initialize|keygen|balance|address|addresses|default_address|secrets|import_secrets|tree|coins}".to_string(), 35, false)), + "wallet --" => Some(("{initialize|keygen|balance|address|addresses|default_address|secrets|import_secrets|tree|coins}".to_string(), 35, false)), + "wallet --default_address " => Some(("{address_id}".to_string(), 35, false)), + "scan --reset " => Some(("{height}".to_string(), 35, false)), _ => None, } } @@ -196,6 +226,7 @@ pub async fn interactive(drk: &DrkPtr, endpoint: &Url, history_path: &str, ex: & "kaching" => kaching().await, "ping" => handle_ping(drk).await, "completions" => handle_completions(&parts), + "wallet" => handle_wallet(drk, &parts).await, "subscribe" => { handle_subscribe( drk, @@ -333,6 +364,7 @@ async fn handle_ping(drk: &DrkPtr) { /// Auxiliary function to define the completions command handling. fn handle_completions(parts: &[&str]) { + // Check correct command structure if parts.len() != 2 { println!("Malformed `completions` command"); println!("Usage: completions {{shell}}"); @@ -344,6 +376,283 @@ fn handle_completions(parts: &[&str]) { } } +/// Auxiliary function to define the wallet command handling. +async fn handle_wallet(drk: &DrkPtr, parts: &[&str]) { + // Check correct command structure + if parts.len() != 2 && parts.len() != 3 { + println!("Malformed `wallet` command"); + println!("Usage: wallet --{{initialize|keygen|balance|address|addresses|default_address|secrets|import_secrets|tree|coins}}"); + return + } + + // Handle command flag + if parts[1] == "--initialize" { + let lock = drk.read().await; + if let Err(e) = lock.initialize_wallet().await { + println!("Error initializing wallet: {e:?}"); + return + } + if let Err(e) = lock.initialize_money().await { + println!("Failed to initialize Money: {e:?}"); + return + } + if let Err(e) = lock.initialize_dao().await { + println!("Failed to initialize DAO: {e:?}"); + return + } + if let Err(e) = lock.initialize_deployooor() { + println!("Failed to initialize Deployooor: {e:?}"); + } + return + } + + if parts[1] == "--keygen" { + if let Err(e) = drk.read().await.money_keygen().await { + println!("Failed to generate keypair: {e:?}"); + } + return + } + + if parts[1] == "--balance" { + let lock = drk.read().await; + let balmap = match lock.money_balance().await { + Ok(m) => m, + Err(e) => { + println!("Failed to fetch balances map: {e:?}"); + return + } + }; + + let aliases_map = match lock.get_aliases_mapped_by_token().await { + Ok(m) => m, + Err(e) => { + println!("Failed to fetch aliases map: {e:?}"); + return + } + }; + + // Create a prettytable with the new data: + let mut table = Table::new(); + table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); + table.set_titles(row!["Token ID", "Aliases", "Balance"]); + for (token_id, balance) in balmap.iter() { + let aliases = match aliases_map.get(token_id) { + Some(a) => a, + None => "-", + }; + + table.add_row(row![ + token_id, + aliases, + encode_base10(*balance, BALANCE_BASE10_DECIMALS) + ]); + } + + if table.is_empty() { + println!("No unspent balances found"); + } else { + println!("{table}"); + } + return + } + + if parts[1] == "--address" { + match drk.read().await.default_address().await { + Ok(address) => println!("{address}"), + Err(e) => println!("Failed to fetch default address: {e:?}"), + } + return + } + + if parts[1] == "--addresses" { + let addresses = match drk.read().await.addresses().await { + Ok(a) => a, + Err(e) => { + println!("Failed to fetch addresses: {e:?}"); + return + } + }; + + // Create a prettytable with the new data: + let mut table = Table::new(); + table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); + table.set_titles(row!["Key ID", "Public Key", "Secret Key", "Is Default"]); + for (key_id, public_key, secret_key, is_default) in addresses { + let is_default = match is_default { + 1 => "*", + _ => "", + }; + table.add_row(row![key_id, public_key, secret_key, is_default]); + } + + if table.is_empty() { + println!("No addresses found"); + } else { + println!("{table}"); + } + return + } + + if parts[1] == "--default_address" { + if parts.len() != 3 { + println!("Malformed `wallet` command"); + println!("Usage: wallet --default_address {{address_id}}"); + } + + let idx = match usize::from_str(parts[2]) { + Ok(i) => i, + Err(e) => { + println!("Invalid address id: {e:?}"); + return + } + }; + + if let Err(e) = drk.read().await.set_default_address(idx) { + println!("Failed to set default address: {e:?}"); + } + return + } + + if parts[1] == "--secrets" { + match drk.read().await.get_money_secrets().await { + Ok(secrets) => { + for secret in secrets { + println!("{secret}"); + } + } + Err(e) => println!("Failed to fetch secrets: {e:?}"), + } + return + } + + if parts[1] == "--import_secrets" { + let mut secrets = vec![]; + // TODO: read from a file here not stdin + let lines = stdin().lines(); + for (i, line) in lines.enumerate() { + if let Ok(line) = line { + let Ok(bytes) = bs58::decode(&line.trim()).into_vec() else { + println!("Warning: Failed to decode secret on line {i}"); + continue + }; + let Ok(secret) = deserialize_async(&bytes).await else { + println!("Warning: Failed to deserialize secret on line {i}"); + continue + }; + secrets.push(secret); + } + } + + match drk.read().await.import_money_secrets(secrets).await { + Ok(pubkeys) => { + for key in pubkeys { + println!("{key}"); + } + } + Err(e) => println!("Failed to import secrets: {e:?}"), + } + return + } + + if parts[1] == "--tree" { + // TODO: write to a file here not stdout + match drk.read().await.get_money_tree().await { + Ok(tree) => println!("{tree:#?}"), + Err(e) => println!("Failed to fetch tree: {e:?}"), + } + return + } + + if parts[1] == "--coins" { + let lock = drk.read().await; + let coins = match lock.get_coins(true).await { + Ok(c) => c, + Err(e) => { + println!("Failed to fetch coins: {e:?}"); + return + } + }; + + if coins.is_empty() { + return + } + + let aliases_map = match lock.get_aliases_mapped_by_token().await { + Ok(m) => m, + Err(e) => { + println!("Failed to fetch aliases map: {e:?}"); + return + } + }; + + let mut table = Table::new(); + table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); + table.set_titles(row![ + "Coin", + "Token ID", + "Aliases", + "Value", + "Spend Hook", + "User Data", + "Creation Height", + "Spent", + "Spent Height", + "Spent TX" + ]); + for coin in coins { + let aliases = match aliases_map.get(&coin.0.note.token_id.to_string()) { + Some(a) => a, + None => "-", + }; + + let spend_hook = if coin.0.note.spend_hook != FuncId::none() { + format!("{}", coin.0.note.spend_hook) + } else { + String::from("-") + }; + + let user_data = if coin.0.note.user_data != pallas::Base::ZERO { + bs58::encode(&serialize_async(&coin.0.note.user_data).await) + .into_string() + .to_string() + } else { + String::from("-") + }; + + let spent_height = match coin.3 { + Some(spent_height) => spent_height.to_string(), + None => String::from("-"), + }; + + table.add_row(row![ + bs58::encode(&serialize_async(&coin.0.coin.inner()).await) + .into_string() + .to_string(), + coin.0.note.token_id, + aliases, + format!( + "{} ({})", + coin.0.note.value, + encode_base10(coin.0.note.value, BALANCE_BASE10_DECIMALS) + ), + spend_hook, + user_data, + coin.1, + coin.2, + spent_height, + coin.4, + ]); + } + + println!("{table}"); + + return + } + + println!("Malformed `wallet` command"); + println!("Usage: wallet --{{initialize|keygen|balance|address|addresses|default_address|secrets|import_secrets|tree|coins}}"); +} + /// Auxiliary function to define the subscribe command handling. async fn handle_subscribe( drk: &DrkPtr, diff --git a/bin/drk/src/main.rs b/bin/drk/src/main.rs index 5f73eb1ff..d62610cbb 100644 --- a/bin/drk/src/main.rs +++ b/bin/drk/src/main.rs @@ -886,12 +886,12 @@ async fn realmain(args: Args, ex: ExecutorPtr) -> Result<()> { if coins { let coins = drk.get_coins(true).await?; - let aliases_map = drk.get_aliases_mapped_by_token().await?; - if coins.is_empty() { return Ok(()) } + let aliases_map = drk.get_aliases_mapped_by_token().await?; + let mut table = Table::new(); table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); table.set_titles(row![