drk/interactive: wallet command handling added

This commit is contained in:
skoupidi
2025-06-17 15:53:01 +03:00
parent 45176ddc9c
commit 668220d47f
2 changed files with 314 additions and 5 deletions

View File

@@ -16,7 +16,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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<String>) {
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<String>) {
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<String>) {
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<String>) {
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,

View File

@@ -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![