|
@@ -16,7 +16,10 @@
|
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
* 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 futures::{select, FutureExt};
|
|
|
use libc::{fcntl, F_GETFL, F_SETFL, O_NONBLOCK};
|
|
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_history_add, linenoise_history_load, linenoise_history_save,
|
|
|
linenoise_set_completion_callback, linenoise_set_hints_callback, LinenoiseState,
|
|
linenoise_set_completion_callback, linenoise_set_hints_callback, LinenoiseState,
|
|
|
};
|
|
};
|
|
|
|
|
+use prettytable::{format, row, Table};
|
|
|
use smol::channel::{unbounded, Receiver, Sender};
|
|
use smol::channel::{unbounded, Receiver, Sender};
|
|
|
use url::Url;
|
|
use url::Url;
|
|
|
|
|
|
|
|
use darkfi::{
|
|
use darkfi::{
|
|
|
cli_desc,
|
|
cli_desc,
|
|
|
system::{msleep, ExecutorPtr, StoppableTask, StoppableTaskPtr},
|
|
system::{msleep, ExecutorPtr, StoppableTask, StoppableTaskPtr},
|
|
|
- util::path::expand_path,
|
|
|
|
|
|
|
+ util::{parse::encode_base10, path::expand_path},
|
|
|
|
|
+ zk::halo2::Field,
|
|
|
Error,
|
|
Error,
|
|
|
};
|
|
};
|
|
|
|
|
+use darkfi_sdk::{crypto::FuncId, pasta::pallas};
|
|
|
|
|
+use darkfi_serial::{deserialize_async, serialize_async};
|
|
|
|
|
|
|
|
use crate::{
|
|
use crate::{
|
|
|
cli_util::{generate_completions, kaching},
|
|
cli_util::{generate_completions, kaching},
|
|
|
|
|
+ money::BALANCE_BASE10_DECIMALS,
|
|
|
rpc::subscribe_blocks,
|
|
rpc::subscribe_blocks,
|
|
|
DrkPtr,
|
|
DrkPtr,
|
|
|
};
|
|
};
|
|
@@ -54,6 +62,7 @@ fn help() {
|
|
|
println!("\tkaching: Fun");
|
|
println!("\tkaching: Fun");
|
|
|
println!("\tping: Send a ping request to the darkfid RPC endpoint");
|
|
println!("\tping: Send a ping request to the darkfid RPC endpoint");
|
|
|
println!("\tcompletions: Generate a SHELL completion script and print to stdout");
|
|
println!("\tcompletions: Generate a SHELL completion script and print to stdout");
|
|
|
|
|
+ println!("\twallet: Wallet operations");
|
|
|
println!(
|
|
println!(
|
|
|
"\tsubscribe: Perform a scan and then subscribe to darkfid to listen for incoming blocks"
|
|
"\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
|
|
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") {
|
|
if buf.starts_with("su") {
|
|
|
lc.push("subscribe".to_string());
|
|
lc.push("subscribe".to_string());
|
|
|
return
|
|
return
|
|
@@ -108,6 +132,7 @@ fn completion(buf: &str, lc: &mut Vec<String>) {
|
|
|
|
|
|
|
|
if buf.starts_with("sc") {
|
|
if buf.starts_with("sc") {
|
|
|
lc.push("scan".to_string());
|
|
lc.push("scan".to_string());
|
|
|
|
|
+ lc.push("scan --reset".to_string());
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -116,6 +141,7 @@ fn completion(buf: &str, lc: &mut Vec<String>) {
|
|
|
lc.push("subscribe".to_string());
|
|
lc.push("subscribe".to_string());
|
|
|
lc.push("snooze".to_string());
|
|
lc.push("snooze".to_string());
|
|
|
lc.push("scan".to_string());
|
|
lc.push("scan".to_string());
|
|
|
|
|
+ lc.push("scan --reset".to_string());
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -129,7 +155,11 @@ fn completion(buf: &str, lc: &mut Vec<String>) {
|
|
|
fn hints(buf: &str) -> Option<(String, i32, bool)> {
|
|
fn hints(buf: &str) -> Option<(String, i32, bool)> {
|
|
|
match buf {
|
|
match buf {
|
|
|
"completions " => Some(("{shell}".to_string(), 35, false)), // 35 = magenta
|
|
"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,
|
|
_ => None,
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -196,6 +226,7 @@ pub async fn interactive(drk: &DrkPtr, endpoint: &Url, history_path: &str, ex: &
|
|
|
"kaching" => kaching().await,
|
|
"kaching" => kaching().await,
|
|
|
"ping" => handle_ping(drk).await,
|
|
"ping" => handle_ping(drk).await,
|
|
|
"completions" => handle_completions(&parts),
|
|
"completions" => handle_completions(&parts),
|
|
|
|
|
+ "wallet" => handle_wallet(drk, &parts).await,
|
|
|
"subscribe" => {
|
|
"subscribe" => {
|
|
|
handle_subscribe(
|
|
handle_subscribe(
|
|
|
drk,
|
|
drk,
|
|
@@ -333,6 +364,7 @@ async fn handle_ping(drk: &DrkPtr) {
|
|
|
|
|
|
|
|
/// Auxiliary function to define the completions command handling.
|
|
/// Auxiliary function to define the completions command handling.
|
|
|
fn handle_completions(parts: &[&str]) {
|
|
fn handle_completions(parts: &[&str]) {
|
|
|
|
|
+ // Check correct command structure
|
|
|
if parts.len() != 2 {
|
|
if parts.len() != 2 {
|
|
|
println!("Malformed `completions` command");
|
|
println!("Malformed `completions` command");
|
|
|
println!("Usage: completions {{shell}}");
|
|
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.
|
|
/// Auxiliary function to define the subscribe command handling.
|
|
|
async fn handle_subscribe(
|
|
async fn handle_subscribe(
|
|
|
drk: &DrkPtr,
|
|
drk: &DrkPtr,
|