/* 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 std::{ fs::{File, OpenOptions}, io::{stdin, BufRead, BufReader, ErrorKind, Read, Seek, SeekFrom, Write}, str::FromStr, }; use futures::{select, FutureExt}; use libc::{fcntl, F_GETFL, F_SETFL, O_NONBLOCK}; 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 rand::rngs::OsRng; use smol::channel::{unbounded, Receiver, Sender}; use url::Url; use darkfi::{ cli_desc, system::{msleep, ExecutorPtr, StoppableTask, StoppableTaskPtr}, util::{ encoding::base64, parse::{decode_base10, encode_base10}, path::expand_path, }, zk::halo2::Field, Error, }; use darkfi_dao_contract::{blockwindow, model::DaoProposalBulla, DaoFunction}; use darkfi_money_contract::model::{Coin, CoinAttributes, TokenId}; use darkfi_sdk::{ crypto::{ note::AeadEncryptedNote, BaseBlind, FuncId, FuncRef, Keypair, PublicKey, SecretKey, DAO_CONTRACT_ID, }, pasta::{group::ff::PrimeField, pallas}, tx::TransactionHash, }; use darkfi_serial::{deserialize_async, serialize_async}; use crate::{ cli_util::{ append_or_print, generate_completions, kaching, parse_token_pair, parse_tx_from_input, parse_value_pair, print_output, }, dao::{DaoParams, ProposalRecord}, money::BALANCE_BASE10_DECIMALS, rpc::subscribe_blocks, swap::PartialSwapData, DrkPtr, }; // TODO: // 1. Create a transactions cache in the wallet db, so you can use it to handle them. /// Auxiliary function to print the help message. fn help(output: &mut Vec) { output.push(String::from(cli_desc!())); output.push(String::from("Commands:")); output.push(String::from("\thelp: Prints the help message")); output.push(String::from("\tkaching: Fun")); output.push(String::from("\tping: Send a ping request to the darkfid RPC endpoint")); output.push(String::from( "\tcompletions: Generate a SHELL completion script and print to stdout", )); output.push(String::from("\twallet: Wallet operations")); output.push(String::from( "\tspend: Read a transaction from stdin and mark its input coins as spent", )); output.push(String::from("\tunspend: Unspend a coin")); output.push(String::from("\ttransfer: Create a payment transaction")); output.push(String::from("\totc: OTC atomic swap")); output.push(String::from("\tdao: DAO functionalities")); output .push(String::from("\tattach-fee: Attach the fee call to a transaction given from stdin")); output.push(String::from("\tinspect: Inspect a transaction from stdin")); output.push(String::from("\tbroadcast: Read a transaction from stdin and broadcast it")); output.push(String::from( "\tsubscribe: Perform a scan and then subscribe to darkfid to listen for incoming blocks", )); output.push(String::from("\tunsubscribe: Stops the background subscription, if its active")); output.push(String::from("\tsnooze: Disables the background subscription messages printing")); output.push(String::from("\tunsnooze: Enables the background subscription messages printing")); output.push(String::from("\tscan: Scan the blockchain and parse relevant transactions")); output.push(String::from("\texplorer: Explorer related subcommands")); output.push(String::from("\talias: Token alias")); output.push(String::from("\ttoken: Token functionalities")); output.push(String::from("\tcontract: Contract functionalities")); } /// Auxiliary function to define the interactive shell completions. fn completion(buffer: &str, lc: &mut Vec) { // Split commands so we always process the last one let commands: Vec<&str> = buffer.split('|').collect(); // Grab the prefix let prefix = if commands.len() > 1 { commands[..commands.len() - 1].join("|") + "| " } else { String::from("") }; let last = commands.last().unwrap().trim_start(); // First we define the specific commands prefixes if last.starts_with("h") { lc.push(prefix + "help"); return } if last.starts_with("k") { lc.push(prefix + "kaching"); return } if last.starts_with("p") { lc.push(prefix + "ping"); return } if last.starts_with("com") { lc.push(prefix + "completions"); return } if last.starts_with("w") { lc.push(prefix.clone() + "wallet"); lc.push(prefix.clone() + "wallet initialize"); lc.push(prefix.clone() + "wallet keygen"); lc.push(prefix.clone() + "wallet balance"); lc.push(prefix.clone() + "wallet address"); lc.push(prefix.clone() + "wallet addresses"); lc.push(prefix.clone() + "wallet default-address"); lc.push(prefix.clone() + "wallet secrets"); lc.push(prefix.clone() + "wallet import-secrets"); lc.push(prefix.clone() + "wallet tree"); lc.push(prefix + "wallet coins"); return } if last.starts_with("sp") { lc.push(prefix + "spend"); return } if last.starts_with("unsp") { lc.push(prefix + "unspend"); return } if last.starts_with("tr") { lc.push(prefix + "transfer"); return } if last.starts_with("o") { lc.push(prefix.clone() + "otc"); lc.push(prefix.clone() + "otc init"); lc.push(prefix.clone() + "otc join"); lc.push(prefix.clone() + "otc inspect"); lc.push(prefix + "otc sign"); return } if last.starts_with("d") { lc.push(prefix.clone() + "dao"); lc.push(prefix.clone() + "dao create"); lc.push(prefix.clone() + "dao view"); lc.push(prefix.clone() + "dao import"); lc.push(prefix.clone() + "dao update-keys"); lc.push(prefix.clone() + "dao list"); lc.push(prefix.clone() + "dao balance"); lc.push(prefix.clone() + "dao mint"); lc.push(prefix.clone() + "dao propose-transfer"); lc.push(prefix.clone() + "dao propose-generic"); lc.push(prefix.clone() + "dao proposals"); lc.push(prefix.clone() + "dao proposal"); lc.push(prefix.clone() + "dao proposal-import"); lc.push(prefix.clone() + "dao vote"); lc.push(prefix.clone() + "dao exec"); lc.push(prefix + "dao spend-hook"); return } if last.starts_with("at") { lc.push(prefix + "attach-fee"); return } if last.starts_with("i") { lc.push(prefix + "inspect"); return } if last.starts_with("b") { lc.push(prefix + "broadcast"); return } if last.starts_with("su") { lc.push(prefix + "subscribe"); return } if last.starts_with("unsu") { lc.push(prefix + "unsubscribe"); return } if last.starts_with("sn") { lc.push(prefix + "snooze"); return } if last.starts_with("unsn") { lc.push(prefix + "unsnooze"); return } if last.starts_with("sc") { lc.push(prefix.clone() + "scan"); lc.push(prefix + "scan --reset"); return } if last.starts_with("e") { lc.push(prefix.clone() + "explorer"); lc.push(prefix.clone() + "explorer fetch-tx"); lc.push(prefix.clone() + "explorer simulate-tx"); lc.push(prefix.clone() + "explorer txs-history"); lc.push(prefix.clone() + "explorer clear-reverted"); lc.push(prefix + "explorer scanned-blocks"); return } if last.starts_with("al") { lc.push(prefix.clone() + "alias"); lc.push(prefix.clone() + "alias add"); lc.push(prefix.clone() + "alias show"); lc.push(prefix + "alias remove"); return } if last.starts_with("to") { lc.push(prefix.clone() + "token"); lc.push(prefix.clone() + "token import"); lc.push(prefix.clone() + "token generate-mint"); lc.push(prefix.clone() + "token list"); lc.push(prefix.clone() + "token mint"); lc.push(prefix + "token freeze"); return } if last.starts_with("con") { lc.push(prefix.clone() + "contract"); lc.push(prefix.clone() + "contract generate-deploy"); lc.push(prefix.clone() + "contract list"); lc.push(prefix.clone() + "contract deploy"); lc.push(prefix + "contract lock"); return } // Now the catch alls if last.starts_with("a") { lc.push(prefix.clone() + "attach-fee"); lc.push(prefix.clone() + "alias"); lc.push(prefix.clone() + "alias add"); lc.push(prefix.clone() + "alias show"); lc.push(prefix + "alias remove"); return } if last.starts_with("c") { lc.push(prefix.clone() + "completions"); lc.push(prefix.clone() + "contract"); lc.push(prefix.clone() + "contract generate-deploy"); lc.push(prefix.clone() + "contract list"); lc.push(prefix.clone() + "contract deploy"); lc.push(prefix + "contract lock"); return } if last.starts_with("s") { lc.push(prefix.clone() + "spend"); lc.push(prefix.clone() + "subscribe"); lc.push(prefix.clone() + "snooze"); lc.push(prefix.clone() + "scan"); lc.push(prefix + "scan --reset"); return } if last.starts_with("t") { lc.push(prefix.clone() + "transfer"); lc.push(prefix.clone() + "token"); lc.push(prefix.clone() + "token import"); lc.push(prefix.clone() + "token generate-mint"); lc.push(prefix.clone() + "token list"); lc.push(prefix.clone() + "token mint"); lc.push(prefix + "token freeze"); return } if last.starts_with("u") { lc.push(prefix.clone() + "unspend"); lc.push(prefix.clone() + "unsubscribe"); lc.push(prefix + "unsnooze"); } } /// Auxiliary function to define the interactive shell hints. fn hints(buffer: &str) -> Option<(String, i32, bool)> { // Split commands so we always process the last one let commands: Vec<&str> = buffer.split('|').collect(); let last = commands.last().unwrap().trim_start(); let color = 35; // magenta let bold = false; match last { "completions " => Some(("".to_string(), color, bold)), "wallet " => Some(("(initialize|keygen|balance|address|addresses|default-address|secrets|import-secrets|tree|coins)".to_string(), color, bold)), "wallet default-address " => Some(("".to_string(), color, bold)), "unspend " => Some(("".to_string(), color, bold)), "transfer " => Some(("[--half-split] [spend_hook] [user_data]".to_string(), color, bold)), "otc " => Some(("(init|join|inspect|sign)".to_string(), color, bold)), "otc init " => Some((" ".to_string(), color, bold)), "dao " => Some(("(create|view|import|update-keys|list|balance|mint|propose-transfer|propose-generic|proposals|proposal|proposal-import|vote|exec|spend-hook)".to_string(), color, bold)), "dao create " => Some((" ".to_string(), color, bold)), "dao import " => Some(("".to_string(), color, bold)), "dao list " => Some(("[name]".to_string(), color, bold)), "dao balance " => Some(("".to_string(), color, bold)), "dao mint " => Some(("".to_string(), color, bold)), "dao propose-transfer " => Some((" [spend-hook] [user-data]".to_string(), color, bold)), "dao propose-generic" => Some((" [user-data]".to_string(), color, bold)), "dao proposals " => Some(("".to_string(), color, bold)), "dao proposal " => Some(("[--(export|mint-proposal)] ".to_string(), color, bold)), "dao vote " => Some((" [vote-weight]".to_string(), color, bold)), "dao exec " => Some(("[--early] ".to_string(), color, bold)), "scan " => Some(("[--reset]".to_string(), color, bold)), "scan --reset " => Some(("".to_string(), color, bold)), "explorer " => Some(("(fetch-tx|simulate-tx|txs-history|clear-reverted|scanned-blocks)".to_string(), color, bold)), "explorer fetch-tx " => Some(("[--encode] ".to_string(), color, bold)), "explorer txs-history " => Some(("[--encode] [tx-hash]".to_string(), color, bold)), "explorer scanned-blocks " => Some(("[height]".to_string(), color, bold)), "alias " => Some(("(add|show|remove)".to_string(), color, bold)), "alias add " => Some((" ".to_string(), color, bold)), "alias show " => Some(("[-a, --alias ] [-t, --token ]".to_string(), color, bold)), "alias remove " => Some(("".to_string(), color, bold)), "token " => Some(("(import|generate-mint|list|mint|freeze)".to_string(), color, bold)), "token import " => Some((" ".to_string(), color, bold)), "token mint " => Some((" [spend-hook] [user-data]".to_string(), color, bold)), "token freeze " => Some(("".to_string(), color, bold)), "contract " => Some(("(generate-deploy|list|deploy|lock)".to_string(), color, bold)), "contract deploy " => Some((" ".to_string(), color, bold)), "contract lock " => Some(("".to_string(), color, bold)), _ => None, } } /// Auxiliary function to start provided Drk as an interactive shell. /// Only sane/linenoise terminals are suported. pub async fn interactive(drk: &DrkPtr, endpoint: &Url, history_path: &str, ex: &ExecutorPtr) { // Expand the history file path let history_path = match expand_path(history_path) { Ok(p) => p, Err(e) => { eprintln!("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 mut subscription_active = false; let mut snooze_active = false; let subscription_task = StoppableTask::new(); // Create an unbounded smol channel, so we can have a printing // queue the background task can submit messages to the shell. let (shell_sender, shell_receiver) = unbounded(); // Start the interactive shell loop { // Wait for next line to process let line = listen_for_line(&snooze_active, &shell_receiver).await; // Grab input or end if Ctrl-D or Ctrl-C was pressed let Some(line) = line else { break }; // Check if line is empty if line.is_empty() { continue } // Add line to history linenoise_history_add(&line); // Split commands let commands: Vec<&str> = line.split('|').collect(); // Process each command let mut output = vec![]; 'commands_loop: for (command_index, command) in commands.iter().enumerate() { let mut input = output; output = vec![]; // Check if we have to output to a file let (mut command, file, append) = if command.contains('>') { // Check if we write or append to the file let mut split = ">"; let mut append = false; if command.contains(">>") { split = ">>"; append = true; } // Parse command parts let parts: Vec<&str> = command.split(split).collect(); if parts.len() == 1 || parts[0].contains('>') { output.push(format!("Malformed command output file definition: {command}")); continue } let file = String::from(parts[1..].join("").trim()); if file.is_empty() || file.contains('>') { output.push(format!("Malformed command output file definition: {command}")); continue } (parts[0], Some(file), append) } else { (*command, None, false) }; // Check if we have to use a file as input if command.contains('<') { // Parse command parts let parts: Vec<&str> = command.split('<').collect(); if parts.len() == 1 { output.push(format!("Malformed command input file definition: {command}")); continue } // Read the input file name let file = String::from(parts[1..].join("").trim()); if file.is_empty() { output.push(format!("Malformed command input file definition: {command}")); continue } // Expand the input file path let file_path = match expand_path(&file) { Ok(p) => p, Err(e) => { output.push(format!("Error while expanding input file path: {e}")); continue } }; // Read the file lines let file = match File::open(file_path) { Ok(f) => f, Err(e) => { output.push(format!("Error while openning input file: {e}")); continue } }; input = vec![]; for (index, line) in BufReader::new(file).lines().enumerate() { match line { Ok(l) => input.push(l), Err(e) => { output .push(format!("Error while reading input file line {index}: {e}")); continue 'commands_loop } } } command = parts[0]; } // Parse command parts let parts: Vec<&str> = command.split_whitespace().collect(); if parts.is_empty() { continue } // Handle command match parts[0] { "help" => help(&mut output), "kaching" => kaching().await, "ping" => handle_ping(drk, &mut output).await, "completions" => handle_completions(&parts, &mut output), "wallet" => handle_wallet(drk, &parts, &input, &mut output).await, "spend" => handle_spend(drk, &input, &mut output).await, "unspend" => handle_unspend(drk, &parts, &mut output).await, "transfer" => handle_transfer(drk, &parts, &mut output).await, "otc" => handle_otc(drk, &parts, &input, &mut output).await, "dao" => handle_dao(drk, &parts, &input, &mut output).await, "attach-fee" => handle_attach_fee(drk, &input, &mut output).await, "inspect" => handle_inspect(&input, &mut output).await, "broadcast" => handle_broadcast(drk, &input, &mut output).await, "subscribe" => { handle_subscribe( drk, endpoint, &mut subscription_active, &subscription_task, &shell_sender, ex, &mut output, ) .await } "unsubscribe" => { handle_unsubscribe(&mut subscription_active, &subscription_task, &mut output) .await } "snooze" => snooze_active = true, "unsnooze" => snooze_active = false, "scan" => { handle_scan( drk, &subscription_active, &parts, &mut output, &(command_index + 1 == commands.len() && file.is_none()), ) .await } "explorer" => handle_explorer(drk, &parts, &input, &mut output).await, "alias" => handle_alias(drk, &parts, &mut output).await, "token" => handle_token(drk, &parts, &mut output).await, "contract" => handle_contract(drk, &parts, &mut output).await, _ => output.push(format!("Unreconized command: {}", parts[0])), } // Write output to file, if requested if let Some(file) = file { // Expand the output file path let file_path = match expand_path(&file) { Ok(p) => p, Err(e) => { output.push(format!("Error while expanding output file path: {e}")); continue } }; // Open the file let file = if append { OpenOptions::new().create(true).append(true).open(&file_path) } else { File::create(file_path) }; let mut file = match file { Ok(f) => f, Err(e) => { output.push(format!("Error while openning output file: {e}")); continue } }; // Seek end if we append to it if append { if let Err(e) = file.seek(SeekFrom::End(0)) { output.push(format!("Error while seeking end of output file: {e}")); continue } } // Write the output if let Err(e) = file.write_all((output.join("\n") + "\n").as_bytes()) { output.push(format!("Error while writing output file: {e}")); continue } output = vec![]; } } // Handle last command output print_output(&output); } // Stop the subscription task if its active if subscription_active { subscription_task.stop().await; } // Write history file let _ = linenoise_history_save(history_file); } /// Auxiliary function to listen for linenoise input line and handle /// background task messages. async fn listen_for_line( snooze_active: &bool, shell_receiver: &Receiver>, ) -> Option { // Generate the linoise state structure let mut state = match LinenoiseState::edit_start(-1, -1, "drk> ") { Ok(s) => s, Err(e) => { eprintln!("Error while generating linenoise state: {e}"); return None } }; // Set stdin to non-blocking mode let fd = state.get_fd(); unsafe { let flags = fcntl(fd, F_GETFL, 0); fcntl(fd, F_SETFL, flags | O_NONBLOCK); } // Read until we get a line to process let mut line = None; loop { // Future that polls stdin for input let input_future = async { loop { match state.edit_feed() { Ok(Some(l)) => { line = Some(l); break } Ok(None) => break, Err(e) if e.kind() == ErrorKind::Interrupted => break, Err(e) if e.kind() == ErrorKind::WouldBlock => { // No data available, yield and retry msleep(10).await; continue } Err(e) => { eprintln!("Error while reading linenoise feed: {e}"); break } } } }; // Future that polls the channel let channel_future = async { loop { if !shell_receiver.is_empty() { break } msleep(1000).await; } }; // Manage the futures select! { // When input is ready we break out the loop _ = input_future.fuse() => break, // Manage filled channel _ = channel_future.fuse() => { while !shell_receiver.is_empty() { match shell_receiver.recv().await { Ok(msg) => { // We only print if snooze is inactive, // but have to consume the message regardless, // so the queue gets empty. if *snooze_active { continue } // Hide prompt, print output, show prompt again let _ = state.hide(); for line in msg { println!("{}\r", line.replace("\n", "\n\r")); } let _ = state.show(); } Err(e) => { eprintln!("Error while reading shell receiver channel: {e}"); break } } } } } } // Restore blocking mode unsafe { let flags = fcntl(fd, F_GETFL, 0); fcntl(fd, F_SETFL, flags & !O_NONBLOCK); } let _ = state.edit_stop(); line } /// Auxiliary function to define the ping command handling. async fn handle_ping(drk: &DrkPtr, output: &mut Vec) { if let Err(e) = drk.read().await.ping(output).await { output.push(format!("Error while executing ping command: {e}")) } } /// Auxiliary function to define the completions command handling. fn handle_completions(parts: &[&str], output: &mut Vec) { // Check correct command structure if parts.len() != 2 { output.push(String::from("Malformed `completions` command")); output.push(String::from("Usage: completions ")); return } match generate_completions(parts[1]) { Ok(completions) => output.push(completions), Err(e) => output.push(format!("Error while executing completions command: {e}")), } } /// Auxiliary function to define the wallet command handling. async fn handle_wallet(drk: &DrkPtr, parts: &[&str], input: &[String], output: &mut Vec) { // Check correct command structure if parts.len() < 2 { output.push(String::from("Malformed `wallet` command")); output.push(String::from("Usage: wallet (initialize|keygen|balance|address|addresses|default-address|secrets|import-secrets|tree|coins)")); return } // Handle subcommand match parts[1] { "initialize" => handle_wallet_initialize(drk, output).await, "keygen" => handle_wallet_keygen(drk, output).await, "balance" => handle_wallet_balance(drk, output).await, "address" => handle_wallet_address(drk, output).await, "addresses" => handle_wallet_addresses(drk, output).await, "default-address" => handle_wallet_default_address(drk, parts, output).await, "secrets" => handle_wallet_secrets(drk, output).await, "import-secrets" => handle_wallet_import_secrets(drk, input, output).await, "tree" => handle_wallet_tree(drk, output).await, "coins" => handle_wallet_coins(drk, output).await, _ => { output.push(format!("Unreconized wallet subcommand: {}", parts[1])); output.push(String::from("Usage: wallet (initialize|keygen|balance|address|addresses|default-address|secrets|import-secrets|tree|coins)")); } } } /// Auxiliary function to define the wallet initialize subcommand handling. async fn handle_wallet_initialize(drk: &DrkPtr, output: &mut Vec) { let lock = drk.read().await; if let Err(e) = lock.initialize_wallet().await { output.push(format!("Error initializing wallet: {e}")); return } if let Err(e) = lock.initialize_money(output).await { output.push(format!("Failed to initialize Money: {e}")); return } if let Err(e) = lock.initialize_dao().await { output.push(format!("Failed to initialize DAO: {e}")); return } if let Err(e) = lock.initialize_deployooor() { output.push(format!("Failed to initialize Deployooor: {e}")); } } /// Auxiliary function to define the wallet keygen subcommand handling. async fn handle_wallet_keygen(drk: &DrkPtr, output: &mut Vec) { if let Err(e) = drk.read().await.money_keygen(output).await { output.push(format!("Failed to generate keypair: {e}")); } } /// Auxiliary function to define the wallet balance subcommand handling. async fn handle_wallet_balance(drk: &DrkPtr, output: &mut Vec) { let lock = drk.read().await; let balmap = match lock.money_balance().await { Ok(m) => m, Err(e) => { output.push(format!("Failed to fetch balances map: {e}")); return } }; let aliases_map = match lock.get_aliases_mapped_by_token().await { Ok(m) => m, Err(e) => { output.push(format!("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() { output.push(String::from("No unspent balances found")); } else { output.push(format!("{table}")); } } /// Auxiliary function to define the wallet address subcommand handling. async fn handle_wallet_address(drk: &DrkPtr, output: &mut Vec) { match drk.read().await.default_address().await { Ok(address) => output.push(format!("{address}")), Err(e) => output.push(format!("Failed to fetch default address: {e}")), } } /// Auxiliary function to define the wallet addresses subcommand handling. async fn handle_wallet_addresses(drk: &DrkPtr, output: &mut Vec) { let addresses = match drk.read().await.addresses().await { Ok(a) => a, Err(e) => { output.push(format!("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() { output.push(String::from("No addresses found")); } else { output.push(format!("{table}")); } } /// Auxiliary function to define the wallet default address subcommand handling. async fn handle_wallet_default_address(drk: &DrkPtr, parts: &[&str], output: &mut Vec) { if parts.len() != 3 { output.push(String::from("Malformed `wallet default-address` subcommand")); output.push(String::from("Usage: wallet default-address ")); return } let index = match usize::from_str(parts[2]) { Ok(i) => i, Err(e) => { output.push(format!("Invalid address id: {e}")); return } }; if let Err(e) = drk.read().await.set_default_address(index) { output.push(format!("Failed to set default address: {e}")); } } /// Auxiliary function to define the wallet secrets subcommand handling. async fn handle_wallet_secrets(drk: &DrkPtr, output: &mut Vec) { match drk.read().await.get_money_secrets().await { Ok(secrets) => { for secret in secrets { output.push(format!("{secret}")); } } Err(e) => output.push(format!("Failed to fetch secrets: {e}")), } } /// Auxiliary function to define the wallet import secrets subcommand handling. async fn handle_wallet_import_secrets(drk: &DrkPtr, input: &[String], output: &mut Vec) { let mut secrets = vec![]; // Parse input or read from stdin if input.is_empty() { for (i, line) in stdin().lines().enumerate() { let Ok(line) = line else { continue }; let Ok(bytes) = bs58::decode(&line.trim()).into_vec() else { output.push(format!("Warning: Failed to decode secret on line {i}")); continue }; let Ok(secret) = deserialize_async(&bytes).await else { output.push(format!("Warning: Failed to deserialize secret on line {i}")); continue }; secrets.push(secret); } } else { for (i, line) in input.iter().enumerate() { let Ok(bytes) = bs58::decode(&line.trim()).into_vec() else { output.push(format!("Warning: Failed to decode secret on line {i}")); continue }; let Ok(secret) = deserialize_async(&bytes).await else { output.push(format!("Warning: Failed to deserialize secret on line {i}")); continue }; secrets.push(secret); } } match drk.read().await.import_money_secrets(secrets, output).await { Ok(pubkeys) => { for key in pubkeys { output.push(format!("{key}")); } } Err(e) => output.push(format!("Failed to import secrets: {e}")), } } /// Auxiliary function to define the wallet tree subcommand handling. async fn handle_wallet_tree(drk: &DrkPtr, output: &mut Vec) { match drk.read().await.get_money_tree().await { Ok(tree) => output.push(format!("{tree:#?}")), Err(e) => output.push(format!("Failed to fetch tree: {e}")), } } /// Auxiliary function to define the wallet coins subcommand handling. async fn handle_wallet_coins(drk: &DrkPtr, output: &mut Vec) { let lock = drk.read().await; let coins = match lock.get_coins(true).await { Ok(c) => c, Err(e) => { output.push(format!("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) => { output.push(format!("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, ]); } output.push(format!("{table}")); } /// Auxiliary function to define the spend command handling. async fn handle_spend(drk: &DrkPtr, input: &[String], output: &mut Vec) { let tx = match parse_tx_from_input(input).await { Ok(t) => t, Err(e) => { output.push(format!("Error while parsing transaction: {e}")); return } }; if let Err(e) = drk.read().await.mark_tx_spend(&tx, output).await { output.push(format!("Failed to mark transaction coins as spent: {e}")) } } /// Auxiliary function to define the unspend command handling. async fn handle_unspend(drk: &DrkPtr, parts: &[&str], output: &mut Vec) { // Check correct command structure if parts.len() != 2 { output.push(String::from("Malformed `unspend` command")); output.push(String::from("Usage: unspend ")); return } let bytes = match bs58::decode(&parts[1]).into_vec() { Ok(b) => b, Err(e) => { output.push(format!("Invalid coin: {e}")); return } }; let bytes: [u8; 32] = match bytes.try_into() { Ok(b) => b, Err(e) => { output.push(format!("Invalid coin: {e:?}")); return } }; let elem: pallas::Base = match pallas::Base::from_repr(bytes).into() { Some(v) => v, None => { output.push(String::from("Invalid coin")); return } }; if let Err(e) = drk.read().await.unspend_coin(&Coin::from(elem)).await { output.push(format!("Failed to mark coin as unspent: {e}")) } } /// Auxiliary function to define the transfer command handling. async fn handle_transfer(drk: &DrkPtr, parts: &[&str], output: &mut Vec) { // Check correct command structure if parts.len() < 4 || parts.len() > 7 { output.push(String::from("Malformed `transfer` command")); output.push(String::from( "Usage: transfer [--half-split] [spend_hook] [user_data]", )); return } // Parse command let mut index = 1; let mut half_split = false; if parts[index] == "--half-split" { half_split = true; index += 1; } let amount = String::from(parts[index]); if let Err(e) = f64::from_str(&amount) { output.push(format!("Invalid amount: {e}")); return } index += 1; let lock = drk.read().await; let token_id = match lock.get_token(String::from(parts[index])).await { Ok(t) => t, Err(e) => { output.push(format!("Invalid token ID: {e}")); return } }; index += 1; let rcpt = match PublicKey::from_str(parts[index]) { Ok(r) => r, Err(e) => { output.push(format!("Invalid recipient: {e}")); return } }; index += 1; let spend_hook = if index < parts.len() { match FuncId::from_str(parts[index]) { Ok(s) => Some(s), Err(e) => { output.push(format!("Invalid spend hook: {e}")); return } } } else { None }; index += 1; let user_data = if index < parts.len() { let bytes = match bs58::decode(&parts[index]).into_vec() { Ok(b) => b, Err(e) => { output.push(format!("Invalid user data: {e}")); return } }; let bytes: [u8; 32] = match bytes.try_into() { Ok(b) => b, Err(e) => { output.push(format!("Invalid user data: {e:?}")); return } }; let elem: pallas::Base = match pallas::Base::from_repr(bytes).into() { Some(v) => v, None => { output.push(String::from("Invalid user data")); return } }; Some(elem) } else { None }; match lock.transfer(&amount, token_id, rcpt, spend_hook, user_data, half_split).await { Ok(t) => output.push(base64::encode(&serialize_async(&t).await)), Err(e) => output.push(format!("Failed to create payment transaction: {e}")), } } /// Auxiliary function to define the otc command handling. async fn handle_otc(drk: &DrkPtr, parts: &[&str], input: &[String], output: &mut Vec) { // Check correct command structure if parts.len() < 2 { output.push(String::from("Malformed `otc` command")); output.push(String::from("Usage: otc (init|join|inspect|sign)")); return } // Handle subcommand match parts[1] { "init" => handle_otc_init(drk, parts, output).await, "join" => handle_otc_join(drk, parts, input, output).await, "inspect" => handle_otc_inspect(drk, parts, input, output).await, "sign" => handle_otc_sign(drk, parts, input, output).await, _ => { output.push(format!("Unreconized OTC subcommand: {}", parts[1])); output.push(String::from("Usage: otc (init|join|inspect|sign)")); } } } /// Auxiliary function to define the otc init subcommand handling. async fn handle_otc_init(drk: &DrkPtr, parts: &[&str], output: &mut Vec) { // Check correct subcommand structure if parts.len() != 4 { output.push(String::from("Malformed `otc init` subcommand")); output.push(String::from("Usage: otc init ")); return } let value_pair = match parse_value_pair(parts[2]) { Ok(v) => v, Err(e) => { output.push(format!("Invalid value pair: {e}")); return } }; let lock = drk.read().await; let token_pair = match parse_token_pair(&lock, parts[3]).await { Ok(t) => t, Err(e) => { output.push(format!("Invalid token pair: {e}")); return } }; match lock.init_swap(value_pair, token_pair, None, None, None).await { Ok(half) => output.push(base64::encode(&serialize_async(&half).await)), Err(e) => output.push(format!("Failed to create swap transaction half: {e}")), } } /// Auxiliary function to define the otc join subcommand handling. async fn handle_otc_join(drk: &DrkPtr, parts: &[&str], input: &[String], output: &mut Vec) { // Check correct subcommand structure if parts.len() != 2 { output.push(String::from("Malformed `otc join` subcommand")); output.push(String::from("Usage: otc join")); return } // Parse line from input or fallback to stdin if its empty let buf = match input.len() { 0 => { let mut buf = String::new(); if let Err(e) = stdin().read_to_string(&mut buf) { output.push(format!("Failed to read from stdin: {e}")); return }; buf } 1 => input[0].clone(), _ => { output.push(String::from("Multiline input provided")); return } }; let Some(bytes) = base64::decode(buf.trim()) else { output.push(String::from("Failed to decode partial swap data")); return }; let partial: PartialSwapData = match deserialize_async(&bytes).await { Ok(p) => p, Err(e) => { output.push(format!("Failed to deserialize partial swap data: {e}")); return } }; match drk.read().await.join_swap(partial, None, None, None).await { Ok(tx) => output.push(base64::encode(&serialize_async(&tx).await)), Err(e) => output.push(format!("Failed to create a join swap transaction: {e}")), } } /// Auxiliary function to define the otc inspect subcommand handling. async fn handle_otc_inspect( drk: &DrkPtr, parts: &[&str], input: &[String], output: &mut Vec, ) { // Check correct subcommand structure if parts.len() != 2 { output.push(String::from("Malformed `otc inspect` subcommand")); output.push(String::from("Usage: otc inspect")); return } // Parse line from input or fallback to stdin if its empty let buf = match input.len() { 0 => { let mut buf = String::new(); if let Err(e) = stdin().read_to_string(&mut buf) { output.push(format!("Failed to read from stdin: {e}")); return }; buf } 1 => input[0].clone(), _ => { output.push(String::from("Multiline input provided")); return } }; let Some(bytes) = base64::decode(buf.trim()) else { output.push(String::from("Failed to decode swap transaction")); return }; if let Err(e) = drk.read().await.inspect_swap(bytes, output).await { output.push(format!("Failed to inspect swap: {e}")); } } /// Auxiliary function to define the otc sign subcommand handling. async fn handle_otc_sign(drk: &DrkPtr, parts: &[&str], input: &[String], output: &mut Vec) { // Check correct subcommand structure if parts.len() != 2 { output.push(String::from("Malformed `otc sign` subcommand")); output.push(String::from("Usage: otc sign")); return } let mut tx = match parse_tx_from_input(input).await { Ok(t) => t, Err(e) => { output.push(format!("Error while parsing transaction: {e}")); return } }; match drk.read().await.sign_swap(&mut tx).await { Ok(_) => output.push(base64::encode(&serialize_async(&tx).await)), Err(e) => output.push(format!("Failed to sign joined swap transaction: {e}")), } } /// Auxiliary function to define the dao command handling. async fn handle_dao(drk: &DrkPtr, parts: &[&str], input: &[String], output: &mut Vec) { // Check correct command structure if parts.len() < 2 { output.push(String::from("Malformed `dao` command")); output.push(String::from("Usage: dao (create|view|import|update-keys|list|balance|mint|propose-transfer|propose-generic|proposals|proposal|proposal-import|vote|exec|spend-hook)")); return } // Handle subcommand match parts[1] { "create" => handle_dao_create(drk, parts, output).await, "view" => handle_dao_view(parts, input, output).await, "import" => handle_dao_import(drk, parts, input, output).await, "update-keys" => handle_dao_update_keys(drk, parts, input, output).await, "list" => handle_dao_list(drk, parts, output).await, "balance" => handle_dao_balance(drk, parts, output).await, "mint" => handle_dao_mint(drk, parts, output).await, "propose-transfer" => handle_dao_propose_transfer(drk, parts, output).await, "propose-generic" => handle_dao_propose_generic(drk, parts, output).await, "proposals" => handle_dao_proposals(drk, parts, output).await, "proposal" => handle_dao_proposal(drk, parts, output).await, "proposal-import" => handle_dao_proposal_import(drk, parts, input, output).await, "vote" => handle_dao_vote(drk, parts, output).await, "exec" => handle_dao_exec(drk, parts, output).await, "spend-hook" => handle_dao_spend_hook(parts, output).await, _ => { output.push(format!("Unreconized DAO subcommand: {}", parts[1])); output.push(String::from("Usage: dao (create|view|import|update-keys|list|balance|mint|propose-transfer|propose-generic|proposals|proposal|proposal-import|vote|exec|spend-hook)")); } } } /// Auxiliary function to define the dao create subcommand handling. async fn handle_dao_create(drk: &DrkPtr, parts: &[&str], output: &mut Vec) { // Check correct subcommand structure if parts.len() != 7 { output.push(String::from("Malformed `dao create` subcommand")); output.push(String::from("Usage: dao create ")); return } if let Err(e) = f64::from_str(parts[2]) { output.push(format!("Invalid proposer limit: {e}")); return } let proposer_limit = match decode_base10(parts[2], BALANCE_BASE10_DECIMALS, true) { Ok(p) => p, Err(e) => { output.push(format!("Error while parsing proposer limit: {e}")); return } }; if let Err(e) = f64::from_str(parts[3]) { output.push(format!("Invalid quorum: {e}")); return } let quorum = match decode_base10(parts[3], BALANCE_BASE10_DECIMALS, true) { Ok(q) => q, Err(e) => { output.push(format!("Error while parsing quorum: {e}")); return } }; if let Err(e) = f64::from_str(parts[4]) { output.push(format!("Invalid early exec quorum: {e}")); return } let early_exec_quorum = match decode_base10(parts[4], BALANCE_BASE10_DECIMALS, true) { Ok(e) => e, Err(e) => { output.push(format!("Error while parsing early exec quorum: {e}")); return } }; let approval_ratio = match f64::from_str(parts[5]) { Ok(a) => { if a > 1.0 { output.push(String::from("Error: Approval ratio cannot be >1.0")); return } a } Err(e) => { output.push(format!("Invalid approval ratio: {e}")); return } }; let approval_ratio_base = 100_u64; let approval_ratio_quot = (approval_ratio * approval_ratio_base as f64) as u64; let gov_token_id = match drk.read().await.get_token(String::from(parts[6])).await { Ok(g) => g, Err(e) => { output.push(format!("Invalid Token ID: {e}")); return } }; let notes_keypair = Keypair::random(&mut OsRng); let proposer_keypair = Keypair::random(&mut OsRng); let proposals_keypair = Keypair::random(&mut OsRng); let votes_keypair = Keypair::random(&mut OsRng); let exec_keypair = Keypair::random(&mut OsRng); let early_exec_keypair = Keypair::random(&mut OsRng); let bulla_blind = BaseBlind::random(&mut OsRng); let params = DaoParams::new( proposer_limit, quorum, early_exec_quorum, approval_ratio_base, approval_ratio_quot, gov_token_id, Some(notes_keypair.secret), notes_keypair.public, Some(proposer_keypair.secret), proposer_keypair.public, Some(proposals_keypair.secret), proposals_keypair.public, Some(votes_keypair.secret), votes_keypair.public, Some(exec_keypair.secret), exec_keypair.public, Some(early_exec_keypair.secret), early_exec_keypair.public, bulla_blind, ); output.push(params.toml_str()); } /// Auxiliary function to define the dao view subcommand handling. async fn handle_dao_view(parts: &[&str], input: &[String], output: &mut Vec) { // Check correct subcommand structure if parts.len() != 2 { output.push(String::from("Malformed `dao view` subcommand")); output.push(String::from("Usage: dao view")); return } // Parse line from input or fallback to stdin if its empty let buf = match input.len() { 0 => { let mut buf = String::new(); if let Err(e) = stdin().read_to_string(&mut buf) { output.push(format!("Failed to read from stdin: {e}")); return }; buf } 1 => input[0].clone(), _ => { output.push(String::from("Multiline input provided")); return } }; let params = match DaoParams::from_toml_str(&buf) { Ok(p) => p, Err(e) => { output.push(format!("Error while parsing DAO params: {e}")); return } }; output.push(format!("{params}")); } /// Auxiliary function to define the dao import subcommand handling. async fn handle_dao_import( drk: &DrkPtr, parts: &[&str], input: &[String], output: &mut Vec, ) { // Check correct subcommand structure if parts.len() != 3 { output.push(String::from("Malformed `dao import` subcommand")); output.push(String::from("Usage: dao import ")); return } // Parse line from input or fallback to stdin if its empty let buf = match input.len() { 0 => { let mut buf = String::new(); if let Err(e) = stdin().read_to_string(&mut buf) { output.push(format!("Failed to read from stdin: {e}")); return }; buf } 1 => input[0].clone(), _ => { output.push(String::from("Multiline input provided")); return } }; let params = match DaoParams::from_toml_str(&buf) { Ok(p) => p, Err(e) => { output.push(format!("Error while parsing DAO params: {e}")); return } }; if let Err(e) = drk.read().await.import_dao(parts[2], ¶ms, output).await { output.push(format!("Failed to import DAO: {e}")) } } /// Auxiliary function to define the dao update keys subcommand handling. async fn handle_dao_update_keys( drk: &DrkPtr, parts: &[&str], input: &[String], output: &mut Vec, ) { // Check correct subcommand structure if parts.len() != 2 { output.push(String::from("Malformed `dao update-keys` subcommand")); output.push(String::from("Usage: dao update-keys")); return } // Parse line from input or fallback to stdin if its empty let buf = match input.len() { 0 => { let mut buf = String::new(); if let Err(e) = stdin().read_to_string(&mut buf) { output.push(format!("Failed to read from stdin: {e}")); return }; buf } 1 => input[0].clone(), _ => { output.push(String::from("Multiline input provided")); return } }; let params = match DaoParams::from_toml_str(&buf) { Ok(p) => p, Err(e) => { output.push(format!("Error while parsing DAO params: {e}")); return } }; if let Err(e) = drk.read().await.update_dao_keys(¶ms, output).await { output.push(format!("Failed to update DAO keys: {e}")) } } /// Auxiliary function to define the dao list subcommand handling. async fn handle_dao_list(drk: &DrkPtr, parts: &[&str], output: &mut Vec) { // Check correct subcommand structure if parts.len() != 2 && parts.len() != 3 { output.push(String::from("Malformed `dao list` subcommand")); output.push(String::from("Usage: dao list [name]")); return } let name = if parts.len() == 3 { Some(String::from(parts[2])) } else { None }; if let Err(e) = drk.read().await.dao_list(&name, output).await { output.push(format!("Failed to list DAO: {e}")) } } /// Auxiliary function to define the dao balance subcommand handling. async fn handle_dao_balance(drk: &DrkPtr, parts: &[&str], output: &mut Vec) { // Check correct subcommand structure if parts.len() != 3 { output.push(String::from("Malformed `dao balance` subcommand")); output.push(String::from("Usage: dao balance ")); return } let lock = drk.read().await; let balmap = match lock.dao_balance(parts[2]).await { Ok(b) => b, Err(e) => { output.push(format!("Failed to fetch DAO balance: {e}")); return } }; let aliases_map = match lock.get_aliases_mapped_by_token().await { Ok(m) => m, Err(e) => { output.push(format!("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!["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() { output.push(String::from("No unspent balances found")) } else { output.push(format!("{table}")); } } /// Auxiliary function to define the dao mint subcommand handling. async fn handle_dao_mint(drk: &DrkPtr, parts: &[&str], output: &mut Vec) { // Check correct subcommand structure if parts.len() != 3 { output.push(String::from("Malformed `dao mint` subcommand")); output.push(String::from("Usage: dao mint ")); return } match drk.read().await.dao_mint(parts[2]).await { Ok(tx) => output.push(base64::encode(&serialize_async(&tx).await)), Err(e) => output.push(format!("Failed to mint DAO: {e}")), } } /// Auxiliary function to define the dao propose transfer subcommand handling. async fn handle_dao_propose_transfer(drk: &DrkPtr, parts: &[&str], output: &mut Vec) { // Check correct subcommand structure if parts.len() < 7 || parts.len() > 9 { output.push(String::from("Malformed `dao proposal-transfer` subcommand")); output.push(String::from("Usage: dao proposal-transfer [spend-hook] [user-data]")); return } let duration = match u64::from_str(parts[3]) { Ok(d) => d, Err(e) => { output.push(format!("Invalid duration: {e}")); return } }; let amount = String::from(parts[4]); if let Err(e) = f64::from_str(&amount) { output.push(format!("Invalid amount: {e}")); return } let lock = drk.read().await; let token_id = match lock.get_token(String::from(parts[5])).await { Ok(t) => t, Err(e) => { output.push(format!("Invalid token ID: {e}")); return } }; let rcpt = match PublicKey::from_str(parts[6]) { Ok(r) => r, Err(e) => { output.push(format!("Invalid recipient: {e}")); return } }; let mut index = 7; let spend_hook = if index < parts.len() { match FuncId::from_str(parts[index]) { Ok(s) => Some(s), Err(e) => { output.push(format!("Invalid spend hook: {e}")); return } } } else { None }; index += 1; let user_data = if index < parts.len() { let bytes = match bs58::decode(&parts[index]).into_vec() { Ok(b) => b, Err(e) => { output.push(format!("Invalid user data: {e}")); return } }; let bytes: [u8; 32] = match bytes.try_into() { Ok(b) => b, Err(e) => { output.push(format!("Invalid user data: {e:?}")); return } }; let elem: pallas::Base = match pallas::Base::from_repr(bytes).into() { Some(v) => v, None => { output.push(String::from("Invalid user data")); return } }; Some(elem) } else { None }; match drk .read() .await .dao_propose_transfer(parts[2], duration, &amount, token_id, rcpt, spend_hook, user_data) .await { Ok(proposal) => output.push(format!("Generated proposal: {}", proposal.bulla())), Err(e) => output.push(format!("Failed to create DAO transfer proposal: {e}")), } } /// Auxiliary function to define the dao propose generic subcommand handling. async fn handle_dao_propose_generic(drk: &DrkPtr, parts: &[&str], output: &mut Vec) { // Check correct subcommand structure if parts.len() != 4 && parts.len() != 5 { output.push(String::from("Malformed `dao proposal-generic` subcommand")); output.push(String::from("Usage: dao proposal-generic [user-data]")); return } let duration = match u64::from_str(parts[3]) { Ok(d) => d, Err(e) => { output.push(format!("Invalid duration: {e}")); return } }; let user_data = if parts.len() == 5 { let bytes = match bs58::decode(&parts[4]).into_vec() { Ok(b) => b, Err(e) => { output.push(format!("Invalid user data: {e}")); return } }; let bytes: [u8; 32] = match bytes.try_into() { Ok(b) => b, Err(e) => { output.push(format!("Invalid user data: {e:?}")); return } }; let elem: pallas::Base = match pallas::Base::from_repr(bytes).into() { Some(v) => v, None => { output.push(String::from("Invalid user data")); return } }; Some(elem) } else { None }; match drk.read().await.dao_propose_generic(parts[2], duration, user_data).await { Ok(proposal) => output.push(format!("Generated proposal: {}", proposal.bulla())), Err(e) => output.push(format!("Failed to create DAO generic proposal: {e}")), } } /// Auxiliary function to define the dao proposals subcommand handling. async fn handle_dao_proposals(drk: &DrkPtr, parts: &[&str], output: &mut Vec) { // Check correct subcommand structure if parts.len() != 3 { output.push(String::from("Malformed `dao proposals` subcommand")); output.push(String::from("Usage: dao proposals ")); return } match drk.read().await.get_dao_proposals(parts[2]).await { Ok(proposals) => { for (i, proposal) in proposals.iter().enumerate() { output.push(format!("{i}. {}", proposal.bulla())); } } Err(e) => output.push(format!("Failed to retrieve DAO proposals: {e}")), } } /// Auxiliary function to define the dao proposal subcommand handling. async fn handle_dao_proposal(drk: &DrkPtr, parts: &[&str], output: &mut Vec) { // Check correct subcommand structure if parts.len() != 3 && parts.len() != 4 { output.push(String::from("Malformed `dao proposal` subcommand")); output.push(String::from("Usage: dao proposal [--(export|mint-proposal)] ")); return } let mut index = 2; let (export, mint_proposal) = if parts.len() == 4 { index += 1; match parts[index] { "--export" => (true, false), "--mint-proposal" => (false, true), _ => { output.push(String::from("Malformed `dao proposal` subcommand")); output.push(String::from("Usage: dao proposal [--(export|mint-proposal)] ")); return } } } else { (false, false) }; let bulla = match DaoProposalBulla::from_str(parts[index]) { Ok(b) => b, Err(e) => { output.push(format!("Invalid proposal bulla: {e}")); return } }; let lock = drk.read().await; let proposal = match lock.get_dao_proposal_by_bulla(&bulla).await { Ok(p) => p, Err(e) => { output.push(format!("Failed to fetch DAO proposal: {e}")); return } }; if export { // Retrieve the DAO let dao = match lock.get_dao_by_bulla(&proposal.proposal.dao_bulla).await { Ok(d) => d, Err(e) => { output.push(format!("Failed to fetch DAO: {e}")); return } }; // Encypt the proposal let enc_note = AeadEncryptedNote::encrypt(&proposal, &dao.params.dao.proposals_public_key, &mut OsRng) .unwrap(); // Export it to base64 output.push(base64::encode(&serialize_async(&enc_note).await)); return } if mint_proposal { // Identify proposal type by its auth calls for call in &proposal.proposal.auth_calls { // We only support transfer right now if call.function_code == DaoFunction::AuthMoneyTransfer as u8 { match lock.dao_transfer_proposal_tx(&proposal).await { Ok(tx) => output.push(base64::encode(&serialize_async(&tx).await)), Err(e) => output.push(format!("Failed to create DAO transfer proposal: {e}")), } return } } // If proposal has no auth calls, we consider it a generic one if proposal.proposal.auth_calls.is_empty() { match lock.dao_generic_proposal_tx(&proposal).await { Ok(tx) => output.push(base64::encode(&serialize_async(&tx).await)), Err(e) => output.push(format!("Failed to create DAO generic proposal: {e}")), } return } output.push(String::from("Unsuported DAO proposal")); return } output.push(format!("{proposal}")); let mut contract_calls = "\nInvoked contracts:\n".to_string(); for call in proposal.proposal.auth_calls { contract_calls.push_str(&format!( "\tContract: {}\n\tFunction: {}\n\tData: ", call.contract_id, call.function_code )); if call.auth_data.is_empty() { contract_calls.push_str("-\n"); continue; } if call.function_code == DaoFunction::AuthMoneyTransfer as u8 { // We know that the plaintext data live in the data plaintext vec if proposal.data.is_none() { contract_calls.push_str("-\n"); continue; } let coin: CoinAttributes = match deserialize_async(proposal.data.as_ref().unwrap()) .await { Ok(c) => c, Err(e) => { output.push(format!("Failed to deserialize transfer proposal coin data: {e}")); return } }; let spend_hook = if coin.spend_hook == FuncId::none() { "-".to_string() } else { format!("{}", coin.spend_hook) }; let user_data = if coin.user_data == pallas::Base::ZERO { "-".to_string() } else { format!("{:?}", coin.user_data) }; contract_calls.push_str(&format!( "\n\t\t{}: {}\n\t\t{}: {} ({})\n\t\t{}: {}\n\t\t{}: {}\n\t\t{}: {}\n\t\t{}: {}\n\n", "Recipient", coin.public_key, "Amount", coin.value, encode_base10(coin.value, BALANCE_BASE10_DECIMALS), "Token", coin.token_id, "Spend hook", spend_hook, "User data", user_data, "Blind", coin.blind )); } } output.push(contract_calls); let votes = match lock.get_dao_proposal_votes(&bulla).await { Ok(v) => v, Err(e) => { output.push(format!("Failed to fetch DAO proposal votes: {e}")); return } }; let mut total_yes_vote_value = 0; let mut total_no_vote_value = 0; let mut total_all_vote_value = 0; let mut table = Table::new(); table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); table.set_titles(row!["Transaction", "Tokens", "Vote"]); for vote in votes { let vote_option = if vote.vote_option { total_yes_vote_value += vote.all_vote_value; "Yes" } else { total_no_vote_value += vote.all_vote_value; "No" }; total_all_vote_value += vote.all_vote_value; table.add_row(row![ vote.tx_hash, encode_base10(vote.all_vote_value, BALANCE_BASE10_DECIMALS), vote_option ]); } let outcome = if table.is_empty() { output.push(String::from("Votes: No votes found")); "Unknown" } else { output.push(String::from("Votes:")); output.push(format!("{table}")); output.push(format!( "Total tokens votes: {}", encode_base10(total_all_vote_value, BALANCE_BASE10_DECIMALS) )); let approval_ratio = (total_yes_vote_value as f64 * 100.0) / total_all_vote_value as f64; output.push(format!( "Total tokens Yes votes: {} ({approval_ratio:.2}%)", encode_base10(total_yes_vote_value, BALANCE_BASE10_DECIMALS) )); output.push(format!( "Total tokens No votes: {} ({:.2}%)", encode_base10(total_no_vote_value, BALANCE_BASE10_DECIMALS), (total_no_vote_value as f64 * 100.0) / total_all_vote_value as f64 )); let dao = match lock.get_dao_by_bulla(&proposal.proposal.dao_bulla).await { Ok(d) => d, Err(e) => { output.push(format!("Failed to fetch DAO: {e}")); return } }; if total_all_vote_value >= dao.params.dao.quorum && approval_ratio >= (dao.params.dao.approval_ratio_quot / dao.params.dao.approval_ratio_base) as f64 { "Approved" } else { "Rejected" } }; if let Some(exec_tx_hash) = proposal.exec_tx_hash { output.push(format!("Proposal was executed on transaction: {exec_tx_hash}")); return } // Retrieve next block height and current block time target, // to compute their window. let next_block_height = match lock.get_next_block_height().await { Ok(n) => n, Err(e) => { output.push(format!("Failed to fetch next block height: {e}")); return } }; let block_target = match lock.get_block_target().await { Ok(b) => b, Err(e) => { output.push(format!("Failed to fetch block target: {e}")); return } }; let current_window = blockwindow(next_block_height, block_target); let end_time = proposal.proposal.creation_blockwindow + proposal.proposal.duration_blockwindows; let (voting_status, proposal_status_message) = if current_window < end_time { ("Ongoing", format!("Current proposal outcome: {outcome}")) } else { ("Concluded", format!("Proposal outcome: {outcome}")) }; output.push(format!("Voting status: {voting_status}")); output.push(proposal_status_message); } /// Auxiliary function to define the dao proposal import subcommand handling. async fn handle_dao_proposal_import( drk: &DrkPtr, parts: &[&str], input: &[String], output: &mut Vec, ) { // Check correct subcommand structure if parts.len() != 2 { output.push(String::from("Malformed `dao proposal-import` subcommand")); output.push(String::from("Usage: dao proposal-import")); return } // Parse line from input or fallback to stdin if its empty let buf = match input.len() { 0 => { let mut buf = String::new(); if let Err(e) = stdin().read_to_string(&mut buf) { output.push(format!("Failed to read from stdin: {e}")); return }; buf } 1 => input[0].clone(), _ => { output.push(String::from("Multiline input provided")); return } }; let Some(bytes) = base64::decode(buf.trim()) else { output.push(String::from("Failed to decode encrypted proposal data")); return }; let encrypted_proposal: AeadEncryptedNote = match deserialize_async(&bytes).await { Ok(e) => e, Err(e) => { output.push(format!("Failed to deserialize encrypted proposal data: {e}")); return } }; let lock = drk.read().await; let daos = match lock.get_daos().await { Ok(d) => d, Err(e) => { output.push(format!("Failed to retrieve DAOs: {e}")); return } }; for dao in &daos { // Check if we have the proposals key let Some(proposals_secret_key) = dao.params.proposals_secret_key else { continue }; // Try to decrypt the proposal let Ok(proposal) = encrypted_proposal.decrypt::(&proposals_secret_key) else { continue }; let proposal = match lock.get_dao_proposal_by_bulla(&proposal.bulla()).await { Ok(p) => { let mut our_proposal = p; our_proposal.data = proposal.data; our_proposal } Err(_) => proposal, }; if let Err(e) = lock.put_dao_proposal(&proposal).await { output.push(format!("Failed to put DAO proposal: {e}")); } } output.push(String::from("Couldn't decrypt the proposal with out DAO keys")); } /// Auxiliary function to define the dao vote subcommand handling. async fn handle_dao_vote(drk: &DrkPtr, parts: &[&str], output: &mut Vec) { // Check correct subcommand structure if parts.len() != 4 && parts.len() != 5 { output.push(String::from("Malformed `dao vote` subcommand")); output.push(String::from("Usage: dao vote [vote-weight]")); return } let bulla = match DaoProposalBulla::from_str(parts[2]) { Ok(b) => b, Err(e) => { output.push(format!("Invalid proposal bulla: {e}")); return } }; let vote = match u8::from_str(parts[3]) { Ok(v) => { if v > 1 { output.push(String::from("Vote can be either 0 (NO) or 1 (YES)")); return } v != 0 } Err(e) => { output.push(format!("Invalid vote: {e}")); return } }; let weight = if parts.len() == 5 { if let Err(e) = f64::from_str(parts[4]) { output.push(format!("Invalid vote weight: {e}")); return } match decode_base10(parts[4], BALANCE_BASE10_DECIMALS, true) { Ok(w) => Some(w), Err(e) => { output.push(format!("Error while parsing vote weight: {e}")); return } } } else { None }; match drk.read().await.dao_vote(&bulla, vote, weight).await { Ok(tx) => output.push(base64::encode(&serialize_async(&tx).await)), Err(e) => output.push(format!("Failed to create DAO Vote transaction: {e}")), } } /// Auxiliary function to define the dao exec subcommand handling. async fn handle_dao_exec(drk: &DrkPtr, parts: &[&str], output: &mut Vec) { // Check correct subcommand structure if parts.len() != 3 && parts.len() != 4 { output.push(String::from("Malformed `dao exec` subcommand")); output.push(String::from("Usage: dao exec [--early] ")); return } let mut index = 2; let mut early = false; if parts[index] == "--early" { early = true; index += 1; } let bulla = match DaoProposalBulla::from_str(parts[index]) { Ok(b) => b, Err(e) => { output.push(format!("Invalid proposal bulla: {e}")); return } }; let lock = drk.read().await; let proposal = match lock.get_dao_proposal_by_bulla(&bulla).await { Ok(p) => p, Err(e) => { output.push(format!("Failed to fetch DAO proposal: {e}")); return } }; // Identify proposal type by its auth calls for call in &proposal.proposal.auth_calls { // We only support transfer right now if call.function_code == DaoFunction::AuthMoneyTransfer as u8 { match lock.dao_exec_transfer(&proposal, early).await { Ok(tx) => output.push(base64::encode(&serialize_async(&tx).await)), Err(e) => output.push(format!("Failed to execute DAO transfer proposal: {e}")), }; return } } // If proposal has no auth calls, we consider it a generic one if proposal.proposal.auth_calls.is_empty() { match lock.dao_exec_generic(&proposal, early).await { Ok(tx) => output.push(base64::encode(&serialize_async(&tx).await)), Err(e) => output.push(format!("Failed to execute DAO generic proposal: {e}")), }; return } output.push(String::from("Unsuported DAO proposal")); } /// Auxiliary function to define the dao spent hook subcommand handling. async fn handle_dao_spend_hook(parts: &[&str], output: &mut Vec) { // Check correct subcommand structure if parts.len() != 2 { output.push(String::from("Malformed `dao spent-hook` subcommand")); output.push(String::from("Usage: dao spent-hook")); return } let spend_hook = FuncRef { contract_id: *DAO_CONTRACT_ID, func_code: DaoFunction::Exec as u8 }.to_func_id(); output.push(format!("{spend_hook}")); } /// Auxiliary function to define the attach fee command handling. async fn handle_attach_fee(drk: &DrkPtr, input: &[String], output: &mut Vec) { let mut tx = match parse_tx_from_input(input).await { Ok(t) => t, Err(e) => { output.push(format!("Error while parsing transaction: {e}")); return } }; match drk.read().await.attach_fee(&mut tx).await { Ok(_) => output.push(base64::encode(&serialize_async(&tx).await)), Err(e) => output.push(format!("Failed to attach the fee call to the transaction: {e}")), } } /// Auxiliary function to define the inspect command handling. async fn handle_inspect(input: &[String], output: &mut Vec) { match parse_tx_from_input(input).await { Ok(tx) => output.push(format!("{tx:#?}")), Err(e) => output.push(format!("Error while parsing transaction: {e}")), } } /// Auxiliary function to define the broadcast command handling. async fn handle_broadcast(drk: &DrkPtr, input: &[String], output: &mut Vec) { let tx = match parse_tx_from_input(input).await { Ok(t) => t, Err(e) => { output.push(format!("Error while parsing transaction: {e}")); return } }; let lock = drk.read().await; if let Err(e) = lock.simulate_tx(&tx).await { output.push(format!("Failed to simulate tx: {e}")); return }; if let Err(e) = lock.mark_tx_spend(&tx, output).await { output.push(format!("Failed to mark transaction coins as spent: {e}")); return }; match lock.broadcast_tx(&tx, output).await { Ok(txid) => output.push(format!("Transaction ID: {txid}")), Err(e) => output.push(format!("Failed to broadcast transaction: {e}")), } } /// Auxiliary function to define the subscribe command handling. async fn handle_subscribe( drk: &DrkPtr, endpoint: &Url, subscription_active: &mut bool, subscription_task: &StoppableTaskPtr, shell_sender: &Sender>, ex: &ExecutorPtr, output: &mut Vec, ) { if *subscription_active { output.push(String::from("Subscription is already active!")); return } // Start the subcristion task let drk_ = drk.clone(); let endpoint_ = endpoint.clone(); let shell_sender_ = shell_sender.clone(); let ex_ = ex.clone(); subscription_task.clone().start( async move { subscribe_blocks(&drk_, shell_sender_, endpoint_, &ex_).await }, |res| async { match res { Ok(()) | Err(Error::DetachedTaskStopped) => { /* Do nothing */ } Err(e) => println!("Failed starting subscription task: {e}"), } }, Error::DetachedTaskStopped, ex.clone(), ); *subscription_active = true; } /// Auxiliary function to define the unsubscribe command handling. async fn handle_unsubscribe( subscription_active: &mut bool, subscription_task: &StoppableTaskPtr, output: &mut Vec, ) { if !*subscription_active { output.push(String::from("Subscription is already inactive!")); return } subscription_task.stop().await; *subscription_active = false; } /// Auxiliary function to define the scan command handling. async fn handle_scan( drk: &DrkPtr, subscription_active: &bool, parts: &[&str], output: &mut Vec, print: &bool, ) { if *subscription_active { append_or_print(output, None, print, vec![String::from("Subscription is already active!")]) .await; return } // Check correct command structure if parts.len() != 1 && parts.len() != 3 { append_or_print(output, None, print, vec![String::from("Malformed `scan` command")]).await; return } // Check if reset was requested let lock = drk.read().await; if parts.len() == 3 { if parts[1] != "--reset" { append_or_print( output, None, print, vec![ String::from("Malformed `scan` command"), String::from("Usage: scan --reset "), ], ) .await; return } let height = match u32::from_str(parts[2]) { Ok(h) => h, Err(e) => { append_or_print(output, None, print, vec![format!("Invalid reset height: {e}")]) .await; return } }; let mut buf = vec![]; if let Err(e) = lock.reset_to_height(height, &mut buf).await { buf.push(format!("Failed during wallet reset: {e}")); append_or_print(output, None, print, buf).await; return } append_or_print(output, None, print, buf).await; } if let Err(e) = lock.scan_blocks(output, None, print).await { append_or_print(output, None, print, vec![format!("Failed during scanning: {e}")]).await; return } append_or_print(output, None, print, vec![String::from("Finished scanning blockchain")]).await; } /// Auxiliary function to define the explorer command handling. async fn handle_explorer(drk: &DrkPtr, parts: &[&str], input: &[String], output: &mut Vec) { // Check correct command structure if parts.len() < 2 { output.push(String::from("Malformed `explorer` command")); output.push(String::from( "Usage: explorer (fetch-tx|simulate-tx|txs-history|clear-reverted|scanned-blocks)", )); return } // Handle subcommand match parts[1] { "fetch-tx" => handle_explorer_fetch_tx(drk, parts, output).await, "simulate-tx" => handle_explorer_simulate_tx(drk, parts, input, output).await, "txs-history" => handle_explorer_txs_history(drk, parts, output).await, "clear-reverted" => handle_explorer_clear_reverted(drk, parts, output).await, "scanned-blocks" => handle_explorer_scanned_blocks(drk, parts, output).await, _ => { output.push(format!("Unreconized explorer subcommand: {}", parts[1])); output.push(String::from( "Usage: explorer (fetch-tx|simulate-tx|txs-history|clear-reverted|scanned-blocks)", )); } } } /// Auxiliary function to define the explorer fetch transaction subcommand handling. async fn handle_explorer_fetch_tx(drk: &DrkPtr, parts: &[&str], output: &mut Vec) { // Check correct subcommand structure if parts.len() != 3 && parts.len() != 4 { output.push(String::from("Malformed `explorer fetch-tx` subcommand")); output.push(String::from("Usage: explorer fetch-tx [--encode] ")); return } let mut index = 2; let mut encode = false; if parts[index] == "--encode" { encode = true; index += 1; } let hash = match blake3::Hash::from_hex(parts[index]) { Ok(h) => h, Err(e) => { output.push(format!("Invalid transaction hash: {e}")); return } }; let tx_hash = TransactionHash(*hash.as_bytes()); let tx = match drk.read().await.get_tx(&tx_hash).await { Ok(tx) => tx, Err(e) => { output.push(format!("Failed to fetch transaction: {e}")); return } }; let Some(tx) = tx else { output.push(String::from("Transaction was not found")); return }; // Make sure the tx is correct if tx.hash() != tx_hash { output.push(format!("Transaction hash missmatch: {tx_hash} - {}", tx.hash())); return } if encode { output.push(base64::encode(&serialize_async(&tx).await)); return } output.push(format!("Transaction ID: {tx_hash}")); output.push(format!("{tx:?}")); } /// Auxiliary function to define the explorer simulate transaction subcommand handling. async fn handle_explorer_simulate_tx( drk: &DrkPtr, parts: &[&str], input: &[String], output: &mut Vec, ) { // Check correct subcommand structure if parts.len() != 2 { output.push(String::from("Malformed `explorer simulate-tx` subcommand")); output.push(String::from("Usage: explorer simulate-tx")); return } let tx = match parse_tx_from_input(input).await { Ok(t) => t, Err(e) => { output.push(format!("Error while parsing transaction: {e}")); return } }; match drk.read().await.simulate_tx(&tx).await { Ok(is_valid) => { output.push(format!("Transaction ID: {}", tx.hash())); output.push(format!("State: {}", if is_valid { "valid" } else { "invalid" })); } Err(e) => output.push(format!("Failed to simulate tx: {e}")), } } /// Auxiliary function to define the explorer transactions history subcommand handling. async fn handle_explorer_txs_history(drk: &DrkPtr, parts: &[&str], output: &mut Vec) { // Check correct command structure if parts.len() < 2 || parts.len() > 4 { output.push(String::from("Malformed `explorer txs-history` command")); output.push(String::from("Usage: explorer txs-history [--encode] [tx-hash]")); return } let lock = drk.read().await; if parts.len() > 2 { let mut index = 2; let mut encode = false; if parts[index] == "--encode" { encode = true; index += 1; } let (tx_hash, status, block_height, tx) = match lock.get_tx_history_record(parts[index]).await { Ok(i) => i, Err(e) => { output.push(format!("Failed to fetch transaction: {e}")); return } }; if encode { output.push(base64::encode(&serialize_async(&tx).await)); return } output.push(format!("Transaction ID: {tx_hash}")); output.push(format!("Status: {status}")); match block_height { Some(block_height) => output.push(format!("Block height: {block_height}")), None => output.push(String::from("Block height: -")), } output.push(format!("{tx:?}")); return } let map = match lock.get_txs_history() { Ok(m) => m, Err(e) => { output.push(format!("Failed to retrieve transactions history records: {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!["Transaction Hash", "Status", "Block Height"]); for (txs_hash, status, block_height) in map.iter() { let block_height = match block_height { Some(block_height) => block_height.to_string(), None => String::from("-"), }; table.add_row(row![txs_hash, status, block_height]); } if table.is_empty() { output.push(String::from("No transactions found")); } else { output.push(format!("{table}")); } } /// Auxiliary function to define the explorer clear reverted subcommand handling. async fn handle_explorer_clear_reverted(drk: &DrkPtr, parts: &[&str], output: &mut Vec) { // Check correct subcommand structure if parts.len() != 2 { output.push(String::from("Malformed `explorer clear-reverted` subcommand")); output.push(String::from("Usage: explorer clear-reverted")); return } if let Err(e) = drk.read().await.remove_reverted_txs(output) { output.push(format!("Failed to remove reverted transactions: {e}")); } } /// Auxiliary function to define the explorer scanned blocks subcommand handling. async fn handle_explorer_scanned_blocks(drk: &DrkPtr, parts: &[&str], output: &mut Vec) { // Check correct subcommand structure if parts.len() != 2 && parts.len() != 3 { output.push(String::from("Malformed `explorer scanned-blocks` subcommand")); output.push(String::from("Usage: explorer scanned-blocks [height]")); return } let lock = drk.read().await; if parts.len() == 3 { let height = match u32::from_str(parts[2]) { Ok(d) => d, Err(e) => { output.push(format!("Invalid height: {e}")); return } }; match lock.get_scanned_block_hash(&height) { Ok(hash) => { output.push(format!("Height: {height}")); output.push(format!("Hash: {hash}")); } Err(e) => output.push(format!("Failed to retrieve scanned block record: {e}")), }; return } let map = match lock.get_scanned_block_records() { Ok(m) => m, Err(e) => { output.push(format!("Failed to retrieve scanned blocks records: {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!["Height", "Hash"]); for (height, hash) in map.iter() { table.add_row(row![height, hash]); } if table.is_empty() { output.push(String::from("No scanned blocks records found")); } else { output.push(format!("{table}")); } } /// Auxiliary function to define the alias command handling. async fn handle_alias(drk: &DrkPtr, parts: &[&str], output: &mut Vec) { // Check correct command structure if parts.len() < 2 { output.push(String::from("Malformed `alias` command")); output.push(String::from("Usage: alias (add|show|remove)")); return } // Handle subcommand match parts[1] { "add" => handle_alias_add(drk, parts, output).await, "show" => handle_alias_show(drk, parts, output).await, "remove" => handle_alias_remove(drk, parts, output).await, _ => { output.push(format!("Unreconized alias subcommand: {}", parts[1])); output.push(String::from("Usage: alias (add|show|remove)")); } } } /// Auxiliary function to define the alias add subcommand handling. async fn handle_alias_add(drk: &DrkPtr, parts: &[&str], output: &mut Vec) { // Check correct subcommand structure if parts.len() != 4 { output.push(String::from("Malformed `alias add` subcommand")); output.push(String::from("Usage: alias add ")); return } if parts[2].len() > 5 { output.push(String::from("Error: Alias exceeds 5 characters")); return } let token_id = match TokenId::from_str(parts[3]) { Ok(t) => t, Err(e) => { output.push(format!("Invalid Token ID: {e}")); return } }; if let Err(e) = drk.read().await.add_alias(String::from(parts[2]), token_id, output).await { output.push(format!("Failed to add alias: {e}")); } } /// Auxiliary function to define the alias show subcommand handling. async fn handle_alias_show(drk: &DrkPtr, parts: &[&str], output: &mut Vec) { // Check correct command structure if parts.len() != 2 && parts.len() != 4 && parts.len() != 6 { output.push(String::from("Malformed `alias show` command")); output.push(String::from("Usage: alias show [-a, --alias ] [-t, --token ]")); return } let mut alias = None; let mut token_id = None; if parts.len() > 2 { let mut index = 2; if parts[index] == "-a" || parts[index] == "--alias" { alias = Some(String::from(parts[index + 1])); index += 2; } if index < parts.len() && (parts[index] == "-t" || parts[index] == "--token") { match TokenId::from_str(parts[index + 1]) { Ok(t) => token_id = Some(t), Err(e) => { output.push(format!("Invalid Token ID: {e}")); return } }; index += 2; } // Check alias again in case it was after token if index < parts.len() && (parts[index] == "-a" || parts[index] == "--alias") { alias = Some(String::from(parts[index + 1])); } } let map = match drk.read().await.get_aliases(alias, token_id).await { Ok(m) => m, Err(e) => { output.push(format!("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!["Alias", "Token ID"]); for (alias, token_id) in map.iter() { table.add_row(row![alias, token_id]); } if table.is_empty() { output.push(String::from("No aliases found")); } else { output.push(format!("{table}")); } } /// Auxiliary function to define the alias remove subcommand handling. async fn handle_alias_remove(drk: &DrkPtr, parts: &[&str], output: &mut Vec) { // Check correct subcommand structure if parts.len() != 3 { output.push(String::from("Malformed `alias remove` subcommand")); output.push(String::from("Usage: alias remove ")); return } if let Err(e) = drk.read().await.remove_alias(String::from(parts[2]), output).await { output.push(format!("Failed to remove alias: {e}")); } } /// Auxiliary function to define the token command handling. async fn handle_token(drk: &DrkPtr, parts: &[&str], output: &mut Vec) { // Check correct command structure if parts.len() < 2 { output.push(String::from("Malformed `token` command")); output.push(String::from("Usage: token (import|generate-mint|list|mint|freeze)")); return } // Handle subcommand match parts[1] { "import" => handle_token_import(drk, parts, output).await, "generate-mint" => handle_token_generate_mint(drk, parts, output).await, "list" => handle_token_list(drk, parts, output).await, "mint" => handle_token_mint(drk, parts, output).await, "freeze" => handle_token_freeze(drk, parts, output).await, _ => { output.push(format!("Unreconized token subcommand: {}", parts[1])); output.push(String::from("Usage: token (import|generate-mint|list|mint|freeze)")); } } } /// Auxiliary function to define the token import subcommand handling. async fn handle_token_import(drk: &DrkPtr, parts: &[&str], output: &mut Vec) { // Check correct subcommand structure if parts.len() != 4 { output.push(String::from("Malformed `token import` subcommand")); output.push(String::from("Usage: token import ")); return } let mint_authority = match SecretKey::from_str(parts[2]) { Ok(ma) => ma, Err(e) => { output.push(format!("Invalid mint authority: {e}")); return } }; let token_blind = match BaseBlind::from_str(parts[3]) { Ok(tb) => tb, Err(e) => { output.push(format!("Invalid token blind: {e}")); return } }; match drk.read().await.import_mint_authority(mint_authority, token_blind).await { Ok(token_id) => { output.push(format!("Successfully imported mint authority for token ID: {token_id}")) } Err(e) => output.push(format!("Failed to import mint authority: {e}")), } } /// Auxiliary function to define the token generate mint subcommand handling. async fn handle_token_generate_mint(drk: &DrkPtr, parts: &[&str], output: &mut Vec) { // Check correct subcommand structure if parts.len() != 2 { output.push(String::from("Malformed `token generate-mint` subcommand")); output.push(String::from("Usage: token generate-mint")); return } let mint_authority = SecretKey::random(&mut OsRng); let token_blind = BaseBlind::random(&mut OsRng); match drk.read().await.import_mint_authority(mint_authority, token_blind).await { Ok(token_id) => { output.push(format!("Successfully imported mint authority for token ID: {token_id}")) } Err(e) => output.push(format!("Failed to import mint authority: {e}")), } } /// Auxiliary function to define the token list subcommand handling. async fn handle_token_list(drk: &DrkPtr, parts: &[&str], output: &mut Vec) { // Check correct subcommand structure if parts.len() != 2 { output.push(String::from("Malformed `token list` subcommand")); output.push(String::from("Usage: token list")); return } let lock = drk.read().await; let tokens = match lock.get_mint_authorities().await { Ok(m) => m, Err(e) => { output.push(format!("Failed to fetch mint authorities: {e}")); return } }; let aliases_map = match lock.get_aliases_mapped_by_token().await { Ok(m) => m, Err(e) => { output.push(format!("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![ "Token ID", "Aliases", "Mint Authority", "Token Blind", "Frozen", "Freeze Height" ]); for (token_id, authority, blind, frozen, freeze_height) in tokens { let aliases = match aliases_map.get(&token_id.to_string()) { Some(a) => a, None => "-", }; let freeze_height = match freeze_height { Some(freeze_height) => freeze_height.to_string(), None => String::from("-"), }; table.add_row(row![token_id, aliases, authority, blind, frozen, freeze_height]); } if table.is_empty() { output.push(String::from("No tokens found")); } else { output.push(format!("{table}")); } } /// Auxiliary function to define the token mint subcommand handling. async fn handle_token_mint(drk: &DrkPtr, parts: &[&str], output: &mut Vec) { // Check correct command structure if parts.len() < 5 || parts.len() > 7 { output.push(String::from("Malformed `token mint` subcommand")); output.push(String::from( "Usage: token mint [spend-hook] [user-data]", )); return } let amount = String::from(parts[3]); if let Err(e) = f64::from_str(&amount) { output.push(format!("Invalid amount: {e}")); return } let rcpt = match PublicKey::from_str(parts[4]) { Ok(r) => r, Err(e) => { output.push(format!("Invalid recipient: {e}")); return } }; let lock = drk.read().await; let token_id = match lock.get_token(String::from(parts[2])).await { Ok(t) => t, Err(e) => { output.push(format!("Invalid token ID: {e}")); return } }; // Parse command let mut index = 5; let spend_hook = if index < parts.len() { match FuncId::from_str(parts[index]) { Ok(s) => Some(s), Err(e) => { output.push(format!("Invalid spend hook: {e}")); return } } } else { None }; index += 1; let user_data = if index < parts.len() { let bytes = match bs58::decode(&parts[index]).into_vec() { Ok(b) => b, Err(e) => { output.push(format!("Invalid user data: {e}")); return } }; let bytes: [u8; 32] = match bytes.try_into() { Ok(b) => b, Err(e) => { output.push(format!("Invalid user data: {e:?}")); return } }; let elem: pallas::Base = match pallas::Base::from_repr(bytes).into() { Some(v) => v, None => { output.push(String::from("Invalid user data")); return } }; Some(elem) } else { None }; match lock.mint_token(&amount, rcpt, token_id, spend_hook, user_data).await { Ok(t) => output.push(base64::encode(&serialize_async(&t).await)), Err(e) => output.push(format!("Failed to create token mint transaction: {e}")), } } /// Auxiliary function to define the token freeze subcommand handling. async fn handle_token_freeze(drk: &DrkPtr, parts: &[&str], output: &mut Vec) { // Check correct subcommand structure if parts.len() != 3 { output.push(String::from("Malformed `token freeze` subcommand")); output.push(String::from("Usage: token freeze ")); return } let lock = drk.read().await; let token_id = match lock.get_token(String::from(parts[2])).await { Ok(t) => t, Err(e) => { output.push(format!("Invalid token ID: {e}")); return } }; match lock.freeze_token(token_id).await { Ok(t) => output.push(base64::encode(&serialize_async(&t).await)), Err(e) => output.push(format!("Failed to create token freeze transaction: {e}")), } } /// Auxiliary function to define the contract command handling. async fn handle_contract(drk: &DrkPtr, parts: &[&str], output: &mut Vec) { // Check correct command structure if parts.len() < 2 { output.push(String::from("Malformed `contract` command")); output.push(String::from("Usage: contract (generate-deploy|list|deploy|lock)")); return } // Handle subcommand match parts[1] { "generate-deploy" => handle_contract_generate_deploy(drk, parts, output).await, "list" => handle_contract_list(drk, parts, output).await, "deploy" => handle_contract_deploy(drk, parts, output).await, "lock" => handle_contract_lock(drk, parts, output).await, _ => { output.push(format!("Unreconized contract subcommand: {}", parts[1])); output.push(String::from("Usage: contract (generate-deploy|list|deploy|lock)")); } } } /// Auxiliary function to define the contract generate deploy subcommand handling. async fn handle_contract_generate_deploy(drk: &DrkPtr, parts: &[&str], output: &mut Vec) { // Check correct subcommand structure if parts.len() != 2 { output.push(String::from("Malformed `contract generate-deploy` subcommand")); output.push(String::from("Usage: contract generate-deploy")); return } if let Err(e) = drk.read().await.deploy_auth_keygen(output).await { output.push(format!("Error creating deploy auth keypair: {e}")); } } /// Auxiliary function to define the contract list subcommand handling. async fn handle_contract_list(drk: &DrkPtr, parts: &[&str], output: &mut Vec) { // Check correct subcommand structure if parts.len() != 2 { output.push(String::from("Malformed `contract list` subcommand")); output.push(String::from("Usage: contract list")); return } let auths = match drk.read().await.list_deploy_auth().await { Ok(a) => a, Err(e) => { output.push(format!("Failed to fetch deploy authorities: {e}")); return } }; let mut table = Table::new(); table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); table.set_titles(row!["Index", "Contract ID", "Frozen", "Freeze Height"]); for (idx, contract_id, frozen, freeze_height) in auths { let freeze_height = match freeze_height { Some(freeze_height) => freeze_height.to_string(), None => String::from("-"), }; table.add_row(row![idx, contract_id, frozen, freeze_height]); } if table.is_empty() { output.push(String::from("No deploy authorities found")); } else { output.push(format!("{table}")); } } /// Auxiliary function to define the contract deploy subcommand handling. async fn handle_contract_deploy(drk: &DrkPtr, parts: &[&str], output: &mut Vec) { // Check correct subcommand structure if parts.len() != 5 { output.push(String::from("Malformed `contract deploy` subcommand")); output.push(String::from("Usage: contract deploy ")); return } let deploy_auth = match u64::from_str(parts[2]) { Ok(d) => d, Err(e) => { output.push(format!("Invalid deploy authority: {e}")); return } }; // Read the wasm bincode and deploy instruction let file_path = match expand_path(parts[3]) { Ok(p) => p, Err(e) => { output.push(format!("Error while expanding wasm bincode file path: {e}")); return } }; let wasm_bin = match smol::fs::read(file_path).await { Ok(w) => w, Err(e) => { output.push(format!("Error while reading wasm bincode file: {e}")); return } }; let file_path = match expand_path(parts[4]) { Ok(p) => p, Err(e) => { output.push(format!("Error while expanding deploy instruction file path: {e}")); return } }; let deploy_ix = match smol::fs::read(file_path).await { Ok(d) => d, Err(e) => { output.push(format!("Error while reading deploy instruction file: {e}")); return } }; let lock = drk.read().await; let mut tx = match lock.deploy_contract(deploy_auth, wasm_bin, deploy_ix).await { Ok(v) => v, Err(e) => { output.push(format!("Error creating contract deployment tx: {e}")); return } }; match lock.attach_fee(&mut tx).await { Ok(_) => output.push(base64::encode(&serialize_async(&tx).await)), Err(e) => output.push(format!("Failed to attach the fee call to the transaction: {e}")), } } /// Auxiliary function to define the contract lock subcommand handling. async fn handle_contract_lock(drk: &DrkPtr, parts: &[&str], output: &mut Vec) { // Check correct subcommand structure if parts.len() != 3 { output.push(String::from("Malformed `contract lock` subcommand")); output.push(String::from("Usage: contract lock ")); return } let deploy_auth = match u64::from_str(parts[2]) { Ok(d) => d, Err(e) => { output.push(format!("Invalid deploy authority: {e}")); return } }; let lock = drk.read().await; let mut tx = match lock.lock_contract(deploy_auth).await { Ok(v) => v, Err(e) => { output.push(format!("Error creating contract lock tx: {e}")); return } }; match lock.attach_fee(&mut tx).await { Ok(_) => output.push(base64::encode(&serialize_async(&tx).await)), Err(e) => output.push(format!("Failed to attach the fee call to the transaction: {e}")), } }