interactive.rs
1 /* This file is part of DarkFi (https://dark.fi) 2 * 3 * Copyright (C) 2020-2025 Dyne.org foundation 4 * 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU Affero General Public License as 7 * published by the Free Software Foundation, either version 3 of the 8 * License, or (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU Affero General Public License for more details. 14 * 15 * You should have received a copy of the GNU Affero General Public License 16 * along with this program. If not, see <https://www.gnu.org/licenses/>. 17 */ 18 19 use std::{ 20 fs::{File, OpenOptions}, 21 io::{stdin, BufRead, BufReader, ErrorKind, Read, Seek, SeekFrom, Write}, 22 str::FromStr, 23 }; 24 25 use futures::{select, FutureExt}; 26 use libc::{fcntl, F_GETFL, F_SETFL, O_NONBLOCK}; 27 use linenoise_rs::{ 28 linenoise_history_add, linenoise_history_load, linenoise_history_save, 29 linenoise_set_completion_callback, linenoise_set_hints_callback, LinenoiseState, 30 }; 31 use prettytable::{format, row, Table}; 32 use rand::rngs::OsRng; 33 use smol::channel::{unbounded, Receiver, Sender}; 34 use url::Url; 35 36 use darkfi::{ 37 cli_desc, 38 system::{msleep, ExecutorPtr, StoppableTask, StoppableTaskPtr}, 39 util::{ 40 encoding::base64, 41 parse::{decode_base10, encode_base10}, 42 path::expand_path, 43 }, 44 zk::halo2::Field, 45 Error, 46 }; 47 use darkfi_dao_contract::{blockwindow, model::DaoProposalBulla, DaoFunction}; 48 use darkfi_money_contract::model::{Coin, CoinAttributes, TokenId}; 49 use darkfi_sdk::{ 50 crypto::{ 51 note::AeadEncryptedNote, BaseBlind, ContractId, FuncId, FuncRef, Keypair, PublicKey, 52 SecretKey, DAO_CONTRACT_ID, 53 }, 54 pasta::{group::ff::PrimeField, pallas}, 55 tx::TransactionHash, 56 }; 57 use darkfi_serial::{deserialize_async, serialize_async}; 58 59 use crate::{ 60 cli_util::{ 61 append_or_print, generate_completions, kaching, parse_token_pair, parse_tx_from_input, 62 parse_value_pair, print_output, 63 }, 64 dao::{DaoParams, ProposalRecord}, 65 money::BALANCE_BASE10_DECIMALS, 66 rpc::subscribe_blocks, 67 swap::PartialSwapData, 68 DrkPtr, 69 }; 70 71 // TODO: 72 // 1. Create a transactions cache in the wallet db, so you can use it to handle them. 73 74 /// Auxiliary function to print the help message. 75 fn help(output: &mut Vec<String>) { 76 output.push(String::from(cli_desc!())); 77 output.push(String::from("Commands:")); 78 output.push(String::from("\thelp: Prints the help message")); 79 output.push(String::from("\tkaching: Fun")); 80 output.push(String::from("\tping: Send a ping request to the darkfid RPC endpoint")); 81 output.push(String::from( 82 "\tcompletions: Generate a SHELL completion script and print to stdout", 83 )); 84 output.push(String::from("\twallet: Wallet operations")); 85 output.push(String::from( 86 "\tspend: Read a transaction from stdin and mark its input coins as spent", 87 )); 88 output.push(String::from("\tunspend: Unspend a coin")); 89 output.push(String::from("\ttransfer: Create a payment transaction")); 90 output.push(String::from("\totc: OTC atomic swap")); 91 output.push(String::from("\tdao: DAO functionalities")); 92 output 93 .push(String::from("\tattach-fee: Attach the fee call to a transaction given from stdin")); 94 output.push(String::from("\tinspect: Inspect a transaction from stdin")); 95 output.push(String::from("\tbroadcast: Read a transaction from stdin and broadcast it")); 96 output.push(String::from( 97 "\tsubscribe: Perform a scan and then subscribe to darkfid to listen for incoming blocks", 98 )); 99 output.push(String::from("\tunsubscribe: Stops the background subscription, if its active")); 100 output.push(String::from("\tsnooze: Disables the background subscription messages printing")); 101 output.push(String::from("\tunsnooze: Enables the background subscription messages printing")); 102 output.push(String::from("\tscan: Scan the blockchain and parse relevant transactions")); 103 output.push(String::from("\texplorer: Explorer related subcommands")); 104 output.push(String::from("\talias: Token alias")); 105 output.push(String::from("\ttoken: Token functionalities")); 106 output.push(String::from("\tcontract: Contract functionalities")); 107 } 108 109 /// Auxiliary function to define the interactive shell completions. 110 fn completion(buffer: &str, lc: &mut Vec<String>) { 111 // Split commands so we always process the last one 112 let commands: Vec<&str> = buffer.split('|').collect(); 113 // Grab the prefix 114 let prefix = if commands.len() > 1 { 115 commands[..commands.len() - 1].join("|") + "| " 116 } else { 117 String::from("") 118 }; 119 let last = commands.last().unwrap().trim_start(); 120 121 // First we define the specific commands prefixes 122 if last.starts_with("h") { 123 lc.push(prefix + "help"); 124 return 125 } 126 127 if last.starts_with("k") { 128 lc.push(prefix + "kaching"); 129 return 130 } 131 132 if last.starts_with("p") { 133 lc.push(prefix + "ping"); 134 return 135 } 136 137 if last.starts_with("com") { 138 lc.push(prefix + "completions"); 139 return 140 } 141 142 if last.starts_with("w") { 143 lc.push(prefix.clone() + "wallet"); 144 lc.push(prefix.clone() + "wallet initialize"); 145 lc.push(prefix.clone() + "wallet keygen"); 146 lc.push(prefix.clone() + "wallet balance"); 147 lc.push(prefix.clone() + "wallet address"); 148 lc.push(prefix.clone() + "wallet addresses"); 149 lc.push(prefix.clone() + "wallet default-address"); 150 lc.push(prefix.clone() + "wallet secrets"); 151 lc.push(prefix.clone() + "wallet import-secrets"); 152 lc.push(prefix.clone() + "wallet tree"); 153 lc.push(prefix + "wallet coins"); 154 return 155 } 156 157 if last.starts_with("sp") { 158 lc.push(prefix + "spend"); 159 return 160 } 161 162 if last.starts_with("unsp") { 163 lc.push(prefix + "unspend"); 164 return 165 } 166 167 if last.starts_with("tr") { 168 lc.push(prefix + "transfer"); 169 return 170 } 171 172 if last.starts_with("o") { 173 lc.push(prefix.clone() + "otc"); 174 lc.push(prefix.clone() + "otc init"); 175 lc.push(prefix.clone() + "otc join"); 176 lc.push(prefix.clone() + "otc inspect"); 177 lc.push(prefix + "otc sign"); 178 return 179 } 180 181 if last.starts_with("d") { 182 lc.push(prefix.clone() + "dao"); 183 lc.push(prefix.clone() + "dao create"); 184 lc.push(prefix.clone() + "dao view"); 185 lc.push(prefix.clone() + "dao import"); 186 lc.push(prefix.clone() + "dao list"); 187 lc.push(prefix.clone() + "dao balance"); 188 lc.push(prefix.clone() + "dao mint"); 189 lc.push(prefix.clone() + "dao propose-transfer"); 190 lc.push(prefix.clone() + "dao propose-generic"); 191 lc.push(prefix.clone() + "dao proposals"); 192 lc.push(prefix.clone() + "dao proposal"); 193 lc.push(prefix.clone() + "dao proposal-import"); 194 lc.push(prefix.clone() + "dao vote"); 195 lc.push(prefix.clone() + "dao exec"); 196 lc.push(prefix + "dao spend-hook"); 197 return 198 } 199 200 if last.starts_with("at") { 201 lc.push(prefix + "attach-fee"); 202 return 203 } 204 205 if last.starts_with("i") { 206 lc.push(prefix + "inspect"); 207 return 208 } 209 210 if last.starts_with("b") { 211 lc.push(prefix + "broadcast"); 212 return 213 } 214 215 if last.starts_with("su") { 216 lc.push(prefix + "subscribe"); 217 return 218 } 219 220 if last.starts_with("unsu") { 221 lc.push(prefix + "unsubscribe"); 222 return 223 } 224 225 if last.starts_with("sn") { 226 lc.push(prefix + "snooze"); 227 return 228 } 229 230 if last.starts_with("unsn") { 231 lc.push(prefix + "unsnooze"); 232 return 233 } 234 235 if last.starts_with("sc") { 236 lc.push(prefix.clone() + "scan"); 237 lc.push(prefix + "scan --reset"); 238 return 239 } 240 241 if last.starts_with("e") { 242 lc.push(prefix.clone() + "explorer"); 243 lc.push(prefix.clone() + "explorer fetch-tx"); 244 lc.push(prefix.clone() + "explorer simulate-tx"); 245 lc.push(prefix.clone() + "explorer txs-history"); 246 lc.push(prefix.clone() + "explorer clear-reverted"); 247 lc.push(prefix + "explorer scanned-blocks"); 248 return 249 } 250 251 if last.starts_with("al") { 252 lc.push(prefix.clone() + "alias"); 253 lc.push(prefix.clone() + "alias add"); 254 lc.push(prefix.clone() + "alias show"); 255 lc.push(prefix + "alias remove"); 256 return 257 } 258 259 if last.starts_with("to") { 260 lc.push(prefix.clone() + "token"); 261 lc.push(prefix.clone() + "token import"); 262 lc.push(prefix.clone() + "token generate-mint"); 263 lc.push(prefix.clone() + "token list"); 264 lc.push(prefix.clone() + "token mint"); 265 lc.push(prefix + "token freeze"); 266 return 267 } 268 269 if last.starts_with("con") { 270 lc.push(prefix.clone() + "contract"); 271 lc.push(prefix.clone() + "contract generate-deploy"); 272 lc.push(prefix.clone() + "contract list"); 273 lc.push(prefix.clone() + "contract export-data"); 274 lc.push(prefix.clone() + "contract deploy"); 275 lc.push(prefix + "contract lock"); 276 return 277 } 278 279 // Now the catch alls 280 if last.starts_with("a") { 281 lc.push(prefix.clone() + "attach-fee"); 282 lc.push(prefix.clone() + "alias"); 283 lc.push(prefix.clone() + "alias add"); 284 lc.push(prefix.clone() + "alias show"); 285 lc.push(prefix + "alias remove"); 286 return 287 } 288 289 if last.starts_with("c") { 290 lc.push(prefix.clone() + "completions"); 291 lc.push(prefix.clone() + "contract"); 292 lc.push(prefix.clone() + "contract generate-deploy"); 293 lc.push(prefix.clone() + "contract list"); 294 lc.push(prefix.clone() + "contract export-data"); 295 lc.push(prefix.clone() + "contract deploy"); 296 lc.push(prefix + "contract lock"); 297 return 298 } 299 300 if last.starts_with("s") { 301 lc.push(prefix.clone() + "spend"); 302 lc.push(prefix.clone() + "subscribe"); 303 lc.push(prefix.clone() + "snooze"); 304 lc.push(prefix.clone() + "scan"); 305 lc.push(prefix + "scan --reset"); 306 return 307 } 308 309 if last.starts_with("t") { 310 lc.push(prefix.clone() + "transfer"); 311 lc.push(prefix.clone() + "token"); 312 lc.push(prefix.clone() + "token import"); 313 lc.push(prefix.clone() + "token generate-mint"); 314 lc.push(prefix.clone() + "token list"); 315 lc.push(prefix.clone() + "token mint"); 316 lc.push(prefix + "token freeze"); 317 return 318 } 319 320 if last.starts_with("u") { 321 lc.push(prefix.clone() + "unspend"); 322 lc.push(prefix.clone() + "unsubscribe"); 323 lc.push(prefix + "unsnooze"); 324 } 325 } 326 327 /// Auxiliary function to define the interactive shell hints. 328 fn hints(buffer: &str) -> Option<(String, i32, bool)> { 329 // Split commands so we always process the last one 330 let commands: Vec<&str> = buffer.split('|').collect(); 331 let last = commands.last().unwrap().trim_start(); 332 let color = 35; // magenta 333 let bold = false; 334 match last { 335 "completions " => Some(("<shell>".to_string(), color, bold)), 336 "wallet " => Some(("(initialize|keygen|balance|address|addresses|default-address|secrets|import-secrets|tree|coins)".to_string(), color, bold)), 337 "wallet default-address " => Some(("<index>".to_string(), color, bold)), 338 "unspend " => Some(("<coin>".to_string(), color, bold)), 339 "transfer " => Some(("[--half-split] <amount> <token> <recipient> [spend_hook] [user_data]".to_string(), color, bold)), 340 "otc " => Some(("(init|join|inspect|sign)".to_string(), color, bold)), 341 "otc init " => Some(("<value_pair> <token_pair>".to_string(), color, bold)), 342 "dao " => Some(("(create|view|import|list|balance|mint|propose-transfer|propose-generic|proposals|proposal|proposal-import|vote|exec|spend-hook)".to_string(), color, bold)), 343 "dao create " => Some(("<proposer-limit> <quorum> <early-exec-quorum> <approval-ratio> <gov-token-id>".to_string(), color, bold)), 344 "dao import " => Some(("<name>".to_string(), color, bold)), 345 "dao list " => Some(("[name]".to_string(), color, bold)), 346 "dao balance " => Some(("<name>".to_string(), color, bold)), 347 "dao mint " => Some(("<name>".to_string(), color, bold)), 348 "dao propose-transfer " => Some(("<name> <duration> <amount> <token> <recipient> [spend-hook] [user-data]".to_string(), color, bold)), 349 "dao propose-generic" => Some(("<name> <duration> [user-data]".to_string(), color, bold)), 350 "dao proposals " => Some(("<name>".to_string(), color, bold)), 351 "dao proposal " => Some(("[--(export|mint-proposal)] <bulla>".to_string(), color, bold)), 352 "dao vote " => Some(("<bulla> <vote> [vote-weight]".to_string(), color, bold)), 353 "dao exec " => Some(("[--early] <bulla>".to_string(), color, bold)), 354 "scan " => Some(("[--reset]".to_string(), color, bold)), 355 "scan --reset " => Some(("<height>".to_string(), color, bold)), 356 "explorer " => Some(("(fetch-tx|simulate-tx|txs-history|clear-reverted|scanned-blocks)".to_string(), color, bold)), 357 "explorer fetch-tx " => Some(("[--encode] <tx-hash>".to_string(), color, bold)), 358 "explorer txs-history " => Some(("[--encode] [tx-hash]".to_string(), color, bold)), 359 "explorer scanned-blocks " => Some(("[height]".to_string(), color, bold)), 360 "alias " => Some(("(add|show|remove)".to_string(), color, bold)), 361 "alias add " => Some(("<alias> <token>".to_string(), color, bold)), 362 "alias show " => Some(("[-a, --alias <alias>] [-t, --token <token>]".to_string(), color, bold)), 363 "alias remove " => Some(("<alias>".to_string(), color, bold)), 364 "token " => Some(("(import|generate-mint|list|mint|freeze)".to_string(), color, bold)), 365 "token import " => Some(("<secret-key> <token-blind>".to_string(), color, bold)), 366 "token mint " => Some(("<token> <amount> <recipient> [spend-hook] [user-data]".to_string(), color, bold)), 367 "token freeze " => Some(("<token>".to_string(), color, bold)), 368 "contract " => Some(("(generate-deploy|list|export-data|deploy|lock)".to_string(), color, bold)), 369 "contract list " => Some(("[contract-id]".to_string(), color, bold)), 370 "contract export-data " => Some(("<tx-hash>".to_string(), color, bold)), 371 "contract deploy " => Some(("<deploy-auth> <wasm-path> [deploy-ix]".to_string(), color, bold)), 372 "contract lock " => Some(("<deploy-auth>".to_string(), color, bold)), 373 _ => None, 374 } 375 } 376 377 /// Auxiliary function to start provided Drk as an interactive shell. 378 /// Only sane/linenoise terminals are suported. 379 pub async fn interactive(drk: &DrkPtr, endpoint: &Url, history_path: &str, ex: &ExecutorPtr) { 380 // Expand the history file path 381 let history_path = match expand_path(history_path) { 382 Ok(p) => p, 383 Err(e) => { 384 eprintln!("Error while expanding history file path: {e}"); 385 return 386 } 387 }; 388 let history_path = history_path.into_os_string(); 389 let history_file = history_path.to_str().unwrap(); 390 391 // Set the completion callback. This will be called every time the 392 // user uses the <tab> key. 393 linenoise_set_completion_callback(completion); 394 395 // Set the shell hints 396 linenoise_set_hints_callback(hints); 397 398 // Load history from file.The history file is just a plain text file 399 // where entries are separated by newlines. 400 let _ = linenoise_history_load(history_file); 401 402 // Create two detached tasks to use for block subscription 403 let mut subscription_active = false; 404 let mut snooze_active = false; 405 let subscription_tasks = [StoppableTask::new(), StoppableTask::new()]; 406 407 // Create an unbounded smol channel, so we can have a printing 408 // queue the background task can submit messages to the shell. 409 let (shell_sender, shell_receiver) = unbounded(); 410 411 // Start the interactive shell 412 loop { 413 // Wait for next line to process 414 let line = listen_for_line(&snooze_active, &shell_receiver).await; 415 416 // Grab input or end if Ctrl-D or Ctrl-C was pressed 417 let Some(line) = line else { break }; 418 419 // Check if line is empty 420 if line.is_empty() { 421 continue 422 } 423 424 // Add line to history 425 linenoise_history_add(&line); 426 427 // Split commands 428 let commands: Vec<&str> = line.split('|').collect(); 429 430 // Process each command 431 let mut output = vec![]; 432 'commands_loop: for (command_index, command) in commands.iter().enumerate() { 433 let mut input = output; 434 output = vec![]; 435 436 // Check if we have to output to a file 437 let (mut command, file, append) = if command.contains('>') { 438 // Check if we write or append to the file 439 let mut split = ">"; 440 let mut append = false; 441 if command.contains(">>") { 442 split = ">>"; 443 append = true; 444 } 445 446 // Parse command parts 447 let parts: Vec<&str> = command.split(split).collect(); 448 if parts.len() == 1 || parts[0].contains('>') { 449 output.push(format!("Malformed command output file definition: {command}")); 450 continue 451 } 452 let file = String::from(parts[1..].join("").trim()); 453 if file.is_empty() || file.contains('>') { 454 output.push(format!("Malformed command output file definition: {command}")); 455 continue 456 } 457 (parts[0], Some(file), append) 458 } else { 459 (*command, None, false) 460 }; 461 462 // Check if we have to use a file as input 463 if command.contains('<') { 464 // Parse command parts 465 let parts: Vec<&str> = command.split('<').collect(); 466 if parts.len() == 1 { 467 output.push(format!("Malformed command input file definition: {command}")); 468 continue 469 } 470 471 // Read the input file name 472 let file = String::from(parts[1..].join("").trim()); 473 if file.is_empty() { 474 output.push(format!("Malformed command input file definition: {command}")); 475 continue 476 } 477 478 // Expand the input file path 479 let file_path = match expand_path(&file) { 480 Ok(p) => p, 481 Err(e) => { 482 output.push(format!("Error while expanding input file path: {e}")); 483 continue 484 } 485 }; 486 487 // Read the file lines 488 let file = match File::open(file_path) { 489 Ok(f) => f, 490 Err(e) => { 491 output.push(format!("Error while openning input file: {e}")); 492 continue 493 } 494 }; 495 input = vec![]; 496 for (index, line) in BufReader::new(file).lines().enumerate() { 497 match line { 498 Ok(l) => input.push(l), 499 Err(e) => { 500 output 501 .push(format!("Error while reading input file line {index}: {e}")); 502 continue 'commands_loop 503 } 504 } 505 } 506 command = parts[0]; 507 } 508 509 // Parse command parts 510 let parts: Vec<&str> = command.split_whitespace().collect(); 511 if parts.is_empty() { 512 continue 513 } 514 515 // Handle command 516 match parts[0] { 517 "help" => help(&mut output), 518 "kaching" => kaching().await, 519 "ping" => handle_ping(drk, &mut output).await, 520 "completions" => handle_completions(&parts, &mut output), 521 "wallet" => handle_wallet(drk, &parts, &input, &mut output).await, 522 "spend" => handle_spend(drk, &input, &mut output).await, 523 "unspend" => handle_unspend(drk, &parts, &mut output).await, 524 "transfer" => handle_transfer(drk, &parts, &mut output).await, 525 "otc" => handle_otc(drk, &parts, &input, &mut output).await, 526 "dao" => handle_dao(drk, &parts, &input, &mut output).await, 527 "attach-fee" => handle_attach_fee(drk, &input, &mut output).await, 528 "inspect" => handle_inspect(&input, &mut output).await, 529 "broadcast" => handle_broadcast(drk, &input, &mut output).await, 530 "subscribe" => { 531 handle_subscribe( 532 drk, 533 endpoint, 534 &mut subscription_active, 535 &subscription_tasks, 536 &shell_sender, 537 ex, 538 ) 539 .await 540 } 541 "unsubscribe" => { 542 handle_unsubscribe(&mut subscription_active, &subscription_tasks).await 543 } 544 "snooze" => snooze_active = true, 545 "unsnooze" => snooze_active = false, 546 "scan" => { 547 handle_scan( 548 drk, 549 &subscription_active, 550 &parts, 551 &mut output, 552 &(command_index + 1 == commands.len() && file.is_none()), 553 ) 554 .await 555 } 556 "explorer" => handle_explorer(drk, &parts, &input, &mut output).await, 557 "alias" => handle_alias(drk, &parts, &mut output).await, 558 "token" => handle_token(drk, &parts, &mut output).await, 559 "contract" => handle_contract(drk, &parts, &mut output).await, 560 _ => output.push(format!("Unrecognized command: {}", parts[0])), 561 } 562 563 // Write output to file, if requested 564 if let Some(file) = file { 565 // Expand the output file path 566 let file_path = match expand_path(&file) { 567 Ok(p) => p, 568 Err(e) => { 569 output.push(format!("Error while expanding output file path: {e}")); 570 continue 571 } 572 }; 573 574 // Open the file 575 let file = if append { 576 OpenOptions::new().create(true).append(true).open(&file_path) 577 } else { 578 File::create(file_path) 579 }; 580 let mut file = match file { 581 Ok(f) => f, 582 Err(e) => { 583 output.push(format!("Error while openning output file: {e}")); 584 continue 585 } 586 }; 587 588 // Seek end if we append to it 589 if append { 590 if let Err(e) = file.seek(SeekFrom::End(0)) { 591 output.push(format!("Error while seeking end of output file: {e}")); 592 continue 593 } 594 } 595 596 // Write the output 597 if let Err(e) = file.write_all((output.join("\n") + "\n").as_bytes()) { 598 output.push(format!("Error while writing output file: {e}")); 599 continue 600 } 601 output = vec![]; 602 } 603 } 604 605 // Handle last command output 606 print_output(&output); 607 } 608 609 // Stop the subscription tasks if they are active 610 subscription_tasks[0].stop_nowait(); 611 subscription_tasks[1].stop_nowait(); 612 613 // Write history file 614 let _ = linenoise_history_save(history_file); 615 } 616 617 /// Auxiliary function to listen for linenoise input line and handle 618 /// background task messages. 619 async fn listen_for_line( 620 snooze_active: &bool, 621 shell_receiver: &Receiver<Vec<String>>, 622 ) -> Option<String> { 623 // Generate the linoise state structure 624 let mut state = match LinenoiseState::edit_start(-1, -1, "drk> ") { 625 Ok(s) => s, 626 Err(e) => { 627 eprintln!("Error while generating linenoise state: {e}"); 628 return None 629 } 630 }; 631 632 // Set stdin to non-blocking mode 633 let fd = state.get_fd(); 634 unsafe { 635 let flags = fcntl(fd, F_GETFL, 0); 636 fcntl(fd, F_SETFL, flags | O_NONBLOCK); 637 } 638 639 // Read until we get a line to process 640 let mut line = None; 641 loop { 642 // Future that polls stdin for input 643 let input_future = async { 644 loop { 645 match state.edit_feed() { 646 Ok(Some(l)) => { 647 line = Some(l); 648 break 649 } 650 Ok(None) => break, 651 Err(e) if e.kind() == ErrorKind::Interrupted => break, 652 Err(e) if e.kind() == ErrorKind::WouldBlock => { 653 // No data available, yield and retry 654 msleep(10).await; 655 continue 656 } 657 Err(e) => { 658 eprintln!("Error while reading linenoise feed: {e}"); 659 break 660 } 661 } 662 } 663 }; 664 665 // Future that polls the channel 666 let channel_future = async { 667 loop { 668 if !shell_receiver.is_empty() { 669 break 670 } 671 msleep(1000).await; 672 } 673 }; 674 675 // Manage the futures 676 select! { 677 // When input is ready we break out the loop 678 _ = input_future.fuse() => break, 679 // Manage filled channel 680 _ = channel_future.fuse() => { 681 // We only print if snooze is inactive, 682 // but have to consume the message regardless, 683 // so the queue gets empty. 684 if *snooze_active { 685 while !shell_receiver.is_empty() { 686 if let Err(e) = shell_receiver.recv().await { 687 eprintln!("Error while reading shell receiver channel: {e}"); 688 break 689 } 690 } 691 } else { 692 // Hide prompt 693 if let Err(e) = state.hide() { 694 eprintln!("Error while hiding linenoise state: {e}"); 695 break 696 }; 697 698 // Restore blocking mode 699 unsafe { 700 let flags = fcntl(fd, F_GETFL, 0); 701 fcntl(fd, F_SETFL, flags & !O_NONBLOCK); 702 } 703 704 // Print output 705 while !shell_receiver.is_empty() { 706 match shell_receiver.recv().await { 707 Ok(msg) => { 708 for line in msg { 709 println!("{}\r", line.replace("\n", "\n\r")); 710 } 711 } 712 Err(e) => { 713 eprintln!("Error while reading shell receiver channel: {e}"); 714 break 715 } 716 } 717 } 718 719 // Set stdin to non-blocking mode 720 unsafe { 721 let flags = fcntl(fd, F_GETFL, 0); 722 fcntl(fd, F_SETFL, flags | O_NONBLOCK); 723 } 724 725 // Show prompt again 726 if let Err(e) = state.show() { 727 eprintln!("Error while showing linenoise state: {e}"); 728 break 729 }; 730 } 731 } 732 } 733 } 734 735 // Restore blocking mode 736 unsafe { 737 let flags = fcntl(fd, F_GETFL, 0); 738 fcntl(fd, F_SETFL, flags & !O_NONBLOCK); 739 } 740 741 if let Err(e) = state.edit_stop() { 742 eprintln!("Error while stopping linenoise state: {e}"); 743 }; 744 line 745 } 746 747 /// Auxiliary function to define the ping command handling. 748 async fn handle_ping(drk: &DrkPtr, output: &mut Vec<String>) { 749 if let Err(e) = drk.read().await.ping(output).await { 750 output.push(format!("Error while executing ping command: {e}")) 751 } 752 } 753 754 /// Auxiliary function to define the completions command handling. 755 fn handle_completions(parts: &[&str], output: &mut Vec<String>) { 756 // Check correct command structure 757 if parts.len() != 2 { 758 output.push(String::from("Malformed `completions` command")); 759 output.push(String::from("Usage: completions <shell>")); 760 return 761 } 762 763 match generate_completions(parts[1]) { 764 Ok(completions) => output.push(completions), 765 Err(e) => output.push(format!("Error while executing completions command: {e}")), 766 } 767 } 768 769 /// Auxiliary function to define the wallet command handling. 770 async fn handle_wallet(drk: &DrkPtr, parts: &[&str], input: &[String], output: &mut Vec<String>) { 771 // Check correct command structure 772 if parts.len() < 2 { 773 output.push(String::from("Malformed `wallet` command")); 774 output.push(String::from("Usage: wallet (initialize|keygen|balance|address|addresses|default-address|secrets|import-secrets|tree|coins)")); 775 return 776 } 777 778 // Handle subcommand 779 match parts[1] { 780 "initialize" => handle_wallet_initialize(drk, output).await, 781 "keygen" => handle_wallet_keygen(drk, output).await, 782 "balance" => handle_wallet_balance(drk, output).await, 783 "address" => handle_wallet_address(drk, output).await, 784 "addresses" => handle_wallet_addresses(drk, output).await, 785 "default-address" => handle_wallet_default_address(drk, parts, output).await, 786 "secrets" => handle_wallet_secrets(drk, output).await, 787 "import-secrets" => handle_wallet_import_secrets(drk, input, output).await, 788 "tree" => handle_wallet_tree(drk, output).await, 789 "coins" => handle_wallet_coins(drk, output).await, 790 _ => { 791 output.push(format!("Unrecognized wallet subcommand: {}", parts[1])); 792 output.push(String::from("Usage: wallet (initialize|keygen|balance|address|addresses|default-address|secrets|import-secrets|tree|coins)")); 793 } 794 } 795 } 796 797 /// Auxiliary function to define the wallet initialize subcommand handling. 798 async fn handle_wallet_initialize(drk: &DrkPtr, output: &mut Vec<String>) { 799 let lock = drk.read().await; 800 if let Err(e) = lock.initialize_wallet().await { 801 output.push(format!("Error initializing wallet: {e}")); 802 return 803 } 804 if let Err(e) = lock.initialize_money(output).await { 805 output.push(format!("Failed to initialize Money: {e}")); 806 return 807 } 808 if let Err(e) = lock.initialize_dao().await { 809 output.push(format!("Failed to initialize DAO: {e}")); 810 return 811 } 812 if let Err(e) = lock.initialize_deployooor() { 813 output.push(format!("Failed to initialize Deployooor: {e}")); 814 } 815 } 816 817 /// Auxiliary function to define the wallet keygen subcommand handling. 818 async fn handle_wallet_keygen(drk: &DrkPtr, output: &mut Vec<String>) { 819 if let Err(e) = drk.read().await.money_keygen(output).await { 820 output.push(format!("Failed to generate keypair: {e}")); 821 } 822 } 823 824 /// Auxiliary function to define the wallet balance subcommand handling. 825 async fn handle_wallet_balance(drk: &DrkPtr, output: &mut Vec<String>) { 826 let lock = drk.read().await; 827 let balmap = match lock.money_balance().await { 828 Ok(m) => m, 829 Err(e) => { 830 output.push(format!("Failed to fetch balances map: {e}")); 831 return 832 } 833 }; 834 835 let aliases_map = match lock.get_aliases_mapped_by_token().await { 836 Ok(m) => m, 837 Err(e) => { 838 output.push(format!("Failed to fetch aliases map: {e}")); 839 return 840 } 841 }; 842 843 // Create a prettytable with the new data: 844 let mut table = Table::new(); 845 table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); 846 table.set_titles(row!["Token ID", "Aliases", "Balance"]); 847 for (token_id, balance) in balmap.iter() { 848 let aliases = match aliases_map.get(token_id) { 849 Some(a) => a, 850 None => "-", 851 }; 852 853 table.add_row(row![token_id, aliases, encode_base10(*balance, BALANCE_BASE10_DECIMALS)]); 854 } 855 856 if table.is_empty() { 857 output.push(String::from("No unspent balances found")); 858 } else { 859 output.push(format!("{table}")); 860 } 861 } 862 863 /// Auxiliary function to define the wallet address subcommand handling. 864 async fn handle_wallet_address(drk: &DrkPtr, output: &mut Vec<String>) { 865 match drk.read().await.default_address().await { 866 Ok(address) => output.push(format!("{address}")), 867 Err(e) => output.push(format!("Failed to fetch default address: {e}")), 868 } 869 } 870 871 /// Auxiliary function to define the wallet addresses subcommand handling. 872 async fn handle_wallet_addresses(drk: &DrkPtr, output: &mut Vec<String>) { 873 let addresses = match drk.read().await.addresses().await { 874 Ok(a) => a, 875 Err(e) => { 876 output.push(format!("Failed to fetch addresses: {e}")); 877 return 878 } 879 }; 880 881 // Create a prettytable with the new data: 882 let mut table = Table::new(); 883 table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); 884 table.set_titles(row!["Key ID", "Public Key", "Secret Key", "Is Default"]); 885 for (key_id, public_key, secret_key, is_default) in addresses { 886 let is_default = match is_default { 887 1 => "*", 888 _ => "", 889 }; 890 table.add_row(row![key_id, public_key, secret_key, is_default]); 891 } 892 893 if table.is_empty() { 894 output.push(String::from("No addresses found")); 895 } else { 896 output.push(format!("{table}")); 897 } 898 } 899 900 /// Auxiliary function to define the wallet default address subcommand handling. 901 async fn handle_wallet_default_address(drk: &DrkPtr, parts: &[&str], output: &mut Vec<String>) { 902 if parts.len() != 3 { 903 output.push(String::from("Malformed `wallet default-address` subcommand")); 904 output.push(String::from("Usage: wallet default-address <index>")); 905 return 906 } 907 908 let index = match usize::from_str(parts[2]) { 909 Ok(i) => i, 910 Err(e) => { 911 output.push(format!("Invalid address id: {e}")); 912 return 913 } 914 }; 915 916 if let Err(e) = drk.read().await.set_default_address(index) { 917 output.push(format!("Failed to set default address: {e}")); 918 } 919 } 920 921 /// Auxiliary function to define the wallet secrets subcommand handling. 922 async fn handle_wallet_secrets(drk: &DrkPtr, output: &mut Vec<String>) { 923 match drk.read().await.get_money_secrets().await { 924 Ok(secrets) => { 925 for secret in secrets { 926 output.push(format!("{secret}")); 927 } 928 } 929 Err(e) => output.push(format!("Failed to fetch secrets: {e}")), 930 } 931 } 932 933 /// Auxiliary function to define the wallet import secrets subcommand handling. 934 async fn handle_wallet_import_secrets(drk: &DrkPtr, input: &[String], output: &mut Vec<String>) { 935 let mut secrets = vec![]; 936 // Parse input or read from stdin 937 if input.is_empty() { 938 for (i, line) in stdin().lines().enumerate() { 939 let Ok(line) = line else { continue }; 940 941 let Ok(bytes) = bs58::decode(&line.trim()).into_vec() else { 942 output.push(format!("Warning: Failed to decode secret on line {i}")); 943 continue 944 }; 945 let Ok(secret) = deserialize_async(&bytes).await else { 946 output.push(format!("Warning: Failed to deserialize secret on line {i}")); 947 continue 948 }; 949 secrets.push(secret); 950 } 951 } else { 952 for (i, line) in input.iter().enumerate() { 953 let Ok(bytes) = bs58::decode(&line.trim()).into_vec() else { 954 output.push(format!("Warning: Failed to decode secret on line {i}")); 955 continue 956 }; 957 let Ok(secret) = deserialize_async(&bytes).await else { 958 output.push(format!("Warning: Failed to deserialize secret on line {i}")); 959 continue 960 }; 961 secrets.push(secret); 962 } 963 } 964 965 match drk.read().await.import_money_secrets(secrets, output).await { 966 Ok(pubkeys) => { 967 for key in pubkeys { 968 output.push(format!("{key}")); 969 } 970 } 971 Err(e) => output.push(format!("Failed to import secrets: {e}")), 972 } 973 } 974 975 /// Auxiliary function to define the wallet tree subcommand handling. 976 async fn handle_wallet_tree(drk: &DrkPtr, output: &mut Vec<String>) { 977 match drk.read().await.get_money_tree().await { 978 Ok(tree) => output.push(format!("{tree:#?}")), 979 Err(e) => output.push(format!("Failed to fetch tree: {e}")), 980 } 981 } 982 983 /// Auxiliary function to define the wallet coins subcommand handling. 984 async fn handle_wallet_coins(drk: &DrkPtr, output: &mut Vec<String>) { 985 let lock = drk.read().await; 986 let coins = match lock.get_coins(true).await { 987 Ok(c) => c, 988 Err(e) => { 989 output.push(format!("Failed to fetch coins: {e}")); 990 return 991 } 992 }; 993 994 if coins.is_empty() { 995 return 996 } 997 998 let aliases_map = match lock.get_aliases_mapped_by_token().await { 999 Ok(m) => m, 1000 Err(e) => { 1001 output.push(format!("Failed to fetch aliases map: {e}")); 1002 return 1003 } 1004 }; 1005 1006 let mut table = Table::new(); 1007 table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); 1008 table.set_titles(row![ 1009 "Coin", 1010 "Token ID", 1011 "Aliases", 1012 "Value", 1013 "Spend Hook", 1014 "User Data", 1015 "Creation Height", 1016 "Spent", 1017 "Spent Height", 1018 "Spent TX" 1019 ]); 1020 for coin in coins { 1021 let aliases = match aliases_map.get(&coin.0.note.token_id.to_string()) { 1022 Some(a) => a, 1023 None => "-", 1024 }; 1025 1026 let spend_hook = if coin.0.note.spend_hook != FuncId::none() { 1027 format!("{}", coin.0.note.spend_hook) 1028 } else { 1029 String::from("-") 1030 }; 1031 1032 let user_data = if coin.0.note.user_data != pallas::Base::ZERO { 1033 bs58::encode(&serialize_async(&coin.0.note.user_data).await).into_string().to_string() 1034 } else { 1035 String::from("-") 1036 }; 1037 1038 let spent_height = match coin.3 { 1039 Some(spent_height) => spent_height.to_string(), 1040 None => String::from("-"), 1041 }; 1042 1043 table.add_row(row![ 1044 bs58::encode(&serialize_async(&coin.0.coin.inner()).await).into_string().to_string(), 1045 coin.0.note.token_id, 1046 aliases, 1047 format!( 1048 "{} ({})", 1049 coin.0.note.value, 1050 encode_base10(coin.0.note.value, BALANCE_BASE10_DECIMALS) 1051 ), 1052 spend_hook, 1053 user_data, 1054 coin.1, 1055 coin.2, 1056 spent_height, 1057 coin.4, 1058 ]); 1059 } 1060 1061 output.push(format!("{table}")); 1062 } 1063 1064 /// Auxiliary function to define the spend command handling. 1065 async fn handle_spend(drk: &DrkPtr, input: &[String], output: &mut Vec<String>) { 1066 let tx = match parse_tx_from_input(input).await { 1067 Ok(t) => t, 1068 Err(e) => { 1069 output.push(format!("Error while parsing transaction: {e}")); 1070 return 1071 } 1072 }; 1073 1074 if let Err(e) = drk.read().await.mark_tx_spend(&tx, output).await { 1075 output.push(format!("Failed to mark transaction coins as spent: {e}")) 1076 } 1077 } 1078 1079 /// Auxiliary function to define the unspend command handling. 1080 async fn handle_unspend(drk: &DrkPtr, parts: &[&str], output: &mut Vec<String>) { 1081 // Check correct command structure 1082 if parts.len() != 2 { 1083 output.push(String::from("Malformed `unspend` command")); 1084 output.push(String::from("Usage: unspend <coin>")); 1085 return 1086 } 1087 1088 let bytes = match bs58::decode(&parts[1]).into_vec() { 1089 Ok(b) => b, 1090 Err(e) => { 1091 output.push(format!("Invalid coin: {e}")); 1092 return 1093 } 1094 }; 1095 1096 let bytes: [u8; 32] = match bytes.try_into() { 1097 Ok(b) => b, 1098 Err(e) => { 1099 output.push(format!("Invalid coin: {e:?}")); 1100 return 1101 } 1102 }; 1103 1104 let elem: pallas::Base = match pallas::Base::from_repr(bytes).into() { 1105 Some(v) => v, 1106 None => { 1107 output.push(String::from("Invalid coin")); 1108 return 1109 } 1110 }; 1111 1112 if let Err(e) = drk.read().await.unspend_coin(&Coin::from(elem)).await { 1113 output.push(format!("Failed to mark coin as unspent: {e}")) 1114 } 1115 } 1116 1117 /// Auxiliary function to define the transfer command handling. 1118 async fn handle_transfer(drk: &DrkPtr, parts: &[&str], output: &mut Vec<String>) { 1119 // Check correct command structure 1120 if parts.len() < 4 || parts.len() > 7 { 1121 output.push(String::from("Malformed `transfer` command")); 1122 output.push(String::from( 1123 "Usage: transfer [--half-split] <amount> <token> <recipient> [spend_hook] [user_data]", 1124 )); 1125 return 1126 } 1127 1128 // Parse command 1129 let mut index = 1; 1130 let mut half_split = false; 1131 if parts[index] == "--half-split" { 1132 half_split = true; 1133 index += 1; 1134 } 1135 1136 let amount = String::from(parts[index]); 1137 if let Err(e) = f64::from_str(&amount) { 1138 output.push(format!("Invalid amount: {e}")); 1139 return 1140 } 1141 index += 1; 1142 1143 let lock = drk.read().await; 1144 let token_id = match lock.get_token(String::from(parts[index])).await { 1145 Ok(t) => t, 1146 Err(e) => { 1147 output.push(format!("Invalid token ID: {e}")); 1148 return 1149 } 1150 }; 1151 index += 1; 1152 1153 let rcpt = match PublicKey::from_str(parts[index]) { 1154 Ok(r) => r, 1155 Err(e) => { 1156 output.push(format!("Invalid recipient: {e}")); 1157 return 1158 } 1159 }; 1160 index += 1; 1161 1162 let spend_hook = if index < parts.len() { 1163 match FuncId::from_str(parts[index]) { 1164 Ok(s) => Some(s), 1165 Err(e) => { 1166 output.push(format!("Invalid spend hook: {e}")); 1167 return 1168 } 1169 } 1170 } else { 1171 None 1172 }; 1173 index += 1; 1174 1175 let user_data = if index < parts.len() { 1176 let bytes = match bs58::decode(&parts[index]).into_vec() { 1177 Ok(b) => b, 1178 Err(e) => { 1179 output.push(format!("Invalid user data: {e}")); 1180 return 1181 } 1182 }; 1183 1184 let bytes: [u8; 32] = match bytes.try_into() { 1185 Ok(b) => b, 1186 Err(e) => { 1187 output.push(format!("Invalid user data: {e:?}")); 1188 return 1189 } 1190 }; 1191 1192 let elem: pallas::Base = match pallas::Base::from_repr(bytes).into() { 1193 Some(v) => v, 1194 None => { 1195 output.push(String::from("Invalid user data")); 1196 return 1197 } 1198 }; 1199 1200 Some(elem) 1201 } else { 1202 None 1203 }; 1204 1205 match lock.transfer(&amount, token_id, rcpt, spend_hook, user_data, half_split).await { 1206 Ok(t) => output.push(base64::encode(&serialize_async(&t).await)), 1207 Err(e) => output.push(format!("Failed to create payment transaction: {e}")), 1208 } 1209 } 1210 1211 /// Auxiliary function to define the otc command handling. 1212 async fn handle_otc(drk: &DrkPtr, parts: &[&str], input: &[String], output: &mut Vec<String>) { 1213 // Check correct command structure 1214 if parts.len() < 2 { 1215 output.push(String::from("Malformed `otc` command")); 1216 output.push(String::from("Usage: otc (init|join|inspect|sign)")); 1217 return 1218 } 1219 1220 // Handle subcommand 1221 match parts[1] { 1222 "init" => handle_otc_init(drk, parts, output).await, 1223 "join" => handle_otc_join(drk, parts, input, output).await, 1224 "inspect" => handle_otc_inspect(drk, parts, input, output).await, 1225 "sign" => handle_otc_sign(drk, parts, input, output).await, 1226 _ => { 1227 output.push(format!("Unrecognized OTC subcommand: {}", parts[1])); 1228 output.push(String::from("Usage: otc (init|join|inspect|sign)")); 1229 } 1230 } 1231 } 1232 1233 /// Auxiliary function to define the otc init subcommand handling. 1234 async fn handle_otc_init(drk: &DrkPtr, parts: &[&str], output: &mut Vec<String>) { 1235 // Check correct subcommand structure 1236 if parts.len() != 4 { 1237 output.push(String::from("Malformed `otc init` subcommand")); 1238 output.push(String::from("Usage: otc init <value_pair> <token_pair>")); 1239 return 1240 } 1241 1242 let value_pair = match parse_value_pair(parts[2]) { 1243 Ok(v) => v, 1244 Err(e) => { 1245 output.push(format!("Invalid value pair: {e}")); 1246 return 1247 } 1248 }; 1249 1250 let lock = drk.read().await; 1251 let token_pair = match parse_token_pair(&lock, parts[3]).await { 1252 Ok(t) => t, 1253 Err(e) => { 1254 output.push(format!("Invalid token pair: {e}")); 1255 return 1256 } 1257 }; 1258 1259 match lock.init_swap(value_pair, token_pair, None, None, None).await { 1260 Ok(half) => output.push(base64::encode(&serialize_async(&half).await)), 1261 Err(e) => output.push(format!("Failed to create swap transaction half: {e}")), 1262 } 1263 } 1264 1265 /// Auxiliary function to define the otc join subcommand handling. 1266 async fn handle_otc_join(drk: &DrkPtr, parts: &[&str], input: &[String], output: &mut Vec<String>) { 1267 // Check correct subcommand structure 1268 if parts.len() != 2 { 1269 output.push(String::from("Malformed `otc join` subcommand")); 1270 output.push(String::from("Usage: otc join")); 1271 return 1272 } 1273 1274 // Parse line from input or fallback to stdin if its empty 1275 let buf = match input.len() { 1276 0 => { 1277 let mut buf = String::new(); 1278 if let Err(e) = stdin().read_to_string(&mut buf) { 1279 output.push(format!("Failed to read from stdin: {e}")); 1280 return 1281 }; 1282 buf 1283 } 1284 1 => input[0].clone(), 1285 _ => { 1286 output.push(String::from("Multiline input provided")); 1287 return 1288 } 1289 }; 1290 1291 let Some(bytes) = base64::decode(buf.trim()) else { 1292 output.push(String::from("Failed to decode partial swap data")); 1293 return 1294 }; 1295 1296 let partial: PartialSwapData = match deserialize_async(&bytes).await { 1297 Ok(p) => p, 1298 Err(e) => { 1299 output.push(format!("Failed to deserialize partial swap data: {e}")); 1300 return 1301 } 1302 }; 1303 1304 match drk.read().await.join_swap(partial, None, None, None).await { 1305 Ok(tx) => output.push(base64::encode(&serialize_async(&tx).await)), 1306 Err(e) => output.push(format!("Failed to create a join swap transaction: {e}")), 1307 } 1308 } 1309 1310 /// Auxiliary function to define the otc inspect subcommand handling. 1311 async fn handle_otc_inspect( 1312 drk: &DrkPtr, 1313 parts: &[&str], 1314 input: &[String], 1315 output: &mut Vec<String>, 1316 ) { 1317 // Check correct subcommand structure 1318 if parts.len() != 2 { 1319 output.push(String::from("Malformed `otc inspect` subcommand")); 1320 output.push(String::from("Usage: otc inspect")); 1321 return 1322 } 1323 1324 // Parse line from input or fallback to stdin if its empty 1325 let buf = match input.len() { 1326 0 => { 1327 let mut buf = String::new(); 1328 if let Err(e) = stdin().read_to_string(&mut buf) { 1329 output.push(format!("Failed to read from stdin: {e}")); 1330 return 1331 }; 1332 buf 1333 } 1334 1 => input[0].clone(), 1335 _ => { 1336 output.push(String::from("Multiline input provided")); 1337 return 1338 } 1339 }; 1340 1341 let Some(bytes) = base64::decode(buf.trim()) else { 1342 output.push(String::from("Failed to decode swap transaction")); 1343 return 1344 }; 1345 1346 if let Err(e) = drk.read().await.inspect_swap(bytes, output).await { 1347 output.push(format!("Failed to inspect swap: {e}")); 1348 } 1349 } 1350 1351 /// Auxiliary function to define the otc sign subcommand handling. 1352 async fn handle_otc_sign(drk: &DrkPtr, parts: &[&str], input: &[String], output: &mut Vec<String>) { 1353 // Check correct subcommand structure 1354 if parts.len() != 2 { 1355 output.push(String::from("Malformed `otc sign` subcommand")); 1356 output.push(String::from("Usage: otc sign")); 1357 return 1358 } 1359 1360 let mut tx = match parse_tx_from_input(input).await { 1361 Ok(t) => t, 1362 Err(e) => { 1363 output.push(format!("Error while parsing transaction: {e}")); 1364 return 1365 } 1366 }; 1367 1368 match drk.read().await.sign_swap(&mut tx).await { 1369 Ok(_) => output.push(base64::encode(&serialize_async(&tx).await)), 1370 Err(e) => output.push(format!("Failed to sign joined swap transaction: {e}")), 1371 } 1372 } 1373 1374 /// Auxiliary function to define the dao command handling. 1375 async fn handle_dao(drk: &DrkPtr, parts: &[&str], input: &[String], output: &mut Vec<String>) { 1376 // Check correct command structure 1377 if parts.len() < 2 { 1378 output.push(String::from("Malformed `dao` command")); 1379 output.push(String::from("Usage: dao (create|view|import|list|balance|mint|propose-transfer|propose-generic|proposals|proposal|proposal-import|vote|exec|spend-hook)")); 1380 return 1381 } 1382 1383 // Handle subcommand 1384 match parts[1] { 1385 "create" => handle_dao_create(drk, parts, output).await, 1386 "view" => handle_dao_view(parts, input, output).await, 1387 "import" => handle_dao_import(drk, parts, input, output).await, 1388 "list" => handle_dao_list(drk, parts, output).await, 1389 "balance" => handle_dao_balance(drk, parts, output).await, 1390 "mint" => handle_dao_mint(drk, parts, output).await, 1391 "propose-transfer" => handle_dao_propose_transfer(drk, parts, output).await, 1392 "propose-generic" => handle_dao_propose_generic(drk, parts, output).await, 1393 "proposals" => handle_dao_proposals(drk, parts, output).await, 1394 "proposal" => handle_dao_proposal(drk, parts, output).await, 1395 "proposal-import" => handle_dao_proposal_import(drk, parts, input, output).await, 1396 "vote" => handle_dao_vote(drk, parts, output).await, 1397 "exec" => handle_dao_exec(drk, parts, output).await, 1398 "spend-hook" => handle_dao_spend_hook(parts, output).await, 1399 _ => { 1400 output.push(format!("Unrecognized DAO subcommand: {}", parts[1])); 1401 output.push(String::from("Usage: dao (create|view|import|list|balance|mint|propose-transfer|propose-generic|proposals|proposal|proposal-import|vote|exec|spend-hook)")); 1402 } 1403 } 1404 } 1405 1406 /// Auxiliary function to define the dao create subcommand handling. 1407 async fn handle_dao_create(drk: &DrkPtr, parts: &[&str], output: &mut Vec<String>) { 1408 // Check correct subcommand structure 1409 if parts.len() != 7 { 1410 output.push(String::from("Malformed `dao create` subcommand")); 1411 output.push(String::from("Usage: dao create <proposer-limit> <quorum> <early-exec-quorum> <approval-ratio> <gov-token-id>")); 1412 return 1413 } 1414 1415 if let Err(e) = f64::from_str(parts[2]) { 1416 output.push(format!("Invalid proposer limit: {e}")); 1417 return 1418 } 1419 let proposer_limit = match decode_base10(parts[2], BALANCE_BASE10_DECIMALS, true) { 1420 Ok(p) => p, 1421 Err(e) => { 1422 output.push(format!("Error while parsing proposer limit: {e}")); 1423 return 1424 } 1425 }; 1426 1427 if let Err(e) = f64::from_str(parts[3]) { 1428 output.push(format!("Invalid quorum: {e}")); 1429 return 1430 } 1431 let quorum = match decode_base10(parts[3], BALANCE_BASE10_DECIMALS, true) { 1432 Ok(q) => q, 1433 Err(e) => { 1434 output.push(format!("Error while parsing quorum: {e}")); 1435 return 1436 } 1437 }; 1438 1439 if let Err(e) = f64::from_str(parts[4]) { 1440 output.push(format!("Invalid early exec quorum: {e}")); 1441 return 1442 } 1443 let early_exec_quorum = match decode_base10(parts[4], BALANCE_BASE10_DECIMALS, true) { 1444 Ok(e) => e, 1445 Err(e) => { 1446 output.push(format!("Error while parsing early exec quorum: {e}")); 1447 return 1448 } 1449 }; 1450 1451 let approval_ratio = match f64::from_str(parts[5]) { 1452 Ok(a) => { 1453 if a > 1.0 { 1454 output.push(String::from("Error: Approval ratio cannot be >1.0")); 1455 return 1456 } 1457 a 1458 } 1459 Err(e) => { 1460 output.push(format!("Invalid approval ratio: {e}")); 1461 return 1462 } 1463 }; 1464 let approval_ratio_base = 100_u64; 1465 let approval_ratio_quot = (approval_ratio * approval_ratio_base as f64) as u64; 1466 1467 let gov_token_id = match drk.read().await.get_token(String::from(parts[6])).await { 1468 Ok(g) => g, 1469 Err(e) => { 1470 output.push(format!("Invalid Token ID: {e}")); 1471 return 1472 } 1473 }; 1474 1475 let notes_keypair = Keypair::random(&mut OsRng); 1476 let proposer_keypair = Keypair::random(&mut OsRng); 1477 let proposals_keypair = Keypair::random(&mut OsRng); 1478 let votes_keypair = Keypair::random(&mut OsRng); 1479 let exec_keypair = Keypair::random(&mut OsRng); 1480 let early_exec_keypair = Keypair::random(&mut OsRng); 1481 let bulla_blind = BaseBlind::random(&mut OsRng); 1482 1483 let params = DaoParams::new( 1484 proposer_limit, 1485 quorum, 1486 early_exec_quorum, 1487 approval_ratio_base, 1488 approval_ratio_quot, 1489 gov_token_id, 1490 Some(notes_keypair.secret), 1491 notes_keypair.public, 1492 Some(proposer_keypair.secret), 1493 proposer_keypair.public, 1494 Some(proposals_keypair.secret), 1495 proposals_keypair.public, 1496 Some(votes_keypair.secret), 1497 votes_keypair.public, 1498 Some(exec_keypair.secret), 1499 exec_keypair.public, 1500 Some(early_exec_keypair.secret), 1501 early_exec_keypair.public, 1502 bulla_blind, 1503 ); 1504 1505 output.push(params.toml_str()); 1506 } 1507 1508 /// Auxiliary function to define the dao view subcommand handling. 1509 async fn handle_dao_view(parts: &[&str], input: &[String], output: &mut Vec<String>) { 1510 // Check correct subcommand structure 1511 if parts.len() != 2 { 1512 output.push(String::from("Malformed `dao view` subcommand")); 1513 output.push(String::from("Usage: dao view")); 1514 return 1515 } 1516 1517 // Parse lines from input or fallback to stdin if its empty 1518 let buf = match input.len() { 1519 0 => { 1520 let mut buf = String::new(); 1521 if let Err(e) = stdin().read_to_string(&mut buf) { 1522 output.push(format!("Failed to read from stdin: {e}")); 1523 return 1524 }; 1525 buf 1526 } 1527 _ => input.join("\n"), 1528 }; 1529 1530 let params = match DaoParams::from_toml_str(&buf) { 1531 Ok(p) => p, 1532 Err(e) => { 1533 output.push(format!("Error while parsing DAO params: {e}")); 1534 return 1535 } 1536 }; 1537 1538 output.push(format!("{params}")); 1539 } 1540 1541 /// Auxiliary function to define the dao import subcommand handling. 1542 async fn handle_dao_import( 1543 drk: &DrkPtr, 1544 parts: &[&str], 1545 input: &[String], 1546 output: &mut Vec<String>, 1547 ) { 1548 // Check correct subcommand structure 1549 if parts.len() != 3 { 1550 output.push(String::from("Malformed `dao import` subcommand")); 1551 output.push(String::from("Usage: dao import <name>")); 1552 return 1553 } 1554 1555 // Parse lines from input or fallback to stdin if its empty 1556 let buf = match input.len() { 1557 0 => { 1558 let mut buf = String::new(); 1559 if let Err(e) = stdin().read_to_string(&mut buf) { 1560 output.push(format!("Failed to read from stdin: {e}")); 1561 return 1562 }; 1563 buf 1564 } 1565 _ => input.join("\n"), 1566 }; 1567 1568 let params = match DaoParams::from_toml_str(&buf) { 1569 Ok(p) => p, 1570 Err(e) => { 1571 output.push(format!("Error while parsing DAO params: {e}")); 1572 return 1573 } 1574 }; 1575 1576 if let Err(e) = drk.read().await.import_dao(parts[2], ¶ms, output).await { 1577 output.push(format!("Failed to import DAO: {e}")) 1578 } 1579 } 1580 1581 /// Auxiliary function to define the dao list subcommand handling. 1582 async fn handle_dao_list(drk: &DrkPtr, parts: &[&str], output: &mut Vec<String>) { 1583 // Check correct subcommand structure 1584 if parts.len() != 2 && parts.len() != 3 { 1585 output.push(String::from("Malformed `dao list` subcommand")); 1586 output.push(String::from("Usage: dao list [name]")); 1587 return 1588 } 1589 1590 let name = if parts.len() == 3 { Some(String::from(parts[2])) } else { None }; 1591 1592 if let Err(e) = drk.read().await.dao_list(&name, output).await { 1593 output.push(format!("Failed to list DAO: {e}")) 1594 } 1595 } 1596 1597 /// Auxiliary function to define the dao balance subcommand handling. 1598 async fn handle_dao_balance(drk: &DrkPtr, parts: &[&str], output: &mut Vec<String>) { 1599 // Check correct subcommand structure 1600 if parts.len() != 3 { 1601 output.push(String::from("Malformed `dao balance` subcommand")); 1602 output.push(String::from("Usage: dao balance <name>")); 1603 return 1604 } 1605 1606 let lock = drk.read().await; 1607 let balmap = match lock.dao_balance(parts[2]).await { 1608 Ok(b) => b, 1609 Err(e) => { 1610 output.push(format!("Failed to fetch DAO balance: {e}")); 1611 return 1612 } 1613 }; 1614 1615 let aliases_map = match lock.get_aliases_mapped_by_token().await { 1616 Ok(m) => m, 1617 Err(e) => { 1618 output.push(format!("Failed to fetch aliases map: {e}")); 1619 return 1620 } 1621 }; 1622 1623 let mut table = Table::new(); 1624 table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); 1625 table.set_titles(row!["Token ID", "Aliases", "Balance"]); 1626 for (token_id, balance) in balmap.iter() { 1627 let aliases = match aliases_map.get(token_id) { 1628 Some(a) => a, 1629 None => "-", 1630 }; 1631 1632 table.add_row(row![token_id, aliases, encode_base10(*balance, BALANCE_BASE10_DECIMALS)]); 1633 } 1634 1635 if table.is_empty() { 1636 output.push(String::from("No unspent balances found")) 1637 } else { 1638 output.push(format!("{table}")); 1639 } 1640 } 1641 1642 /// Auxiliary function to define the dao mint subcommand handling. 1643 async fn handle_dao_mint(drk: &DrkPtr, parts: &[&str], output: &mut Vec<String>) { 1644 // Check correct subcommand structure 1645 if parts.len() != 3 { 1646 output.push(String::from("Malformed `dao mint` subcommand")); 1647 output.push(String::from("Usage: dao mint <name>")); 1648 return 1649 } 1650 1651 match drk.read().await.dao_mint(parts[2]).await { 1652 Ok(tx) => output.push(base64::encode(&serialize_async(&tx).await)), 1653 Err(e) => output.push(format!("Failed to mint DAO: {e}")), 1654 } 1655 } 1656 1657 /// Auxiliary function to define the dao propose transfer subcommand handling. 1658 async fn handle_dao_propose_transfer(drk: &DrkPtr, parts: &[&str], output: &mut Vec<String>) { 1659 // Check correct subcommand structure 1660 if parts.len() < 7 || parts.len() > 9 { 1661 output.push(String::from("Malformed `dao proposal-transfer` subcommand")); 1662 output.push(String::from("Usage: dao proposal-transfer <name> <duration> <amount> <token> <recipient> [spend-hook] [user-data]")); 1663 return 1664 } 1665 1666 let duration = match u64::from_str(parts[3]) { 1667 Ok(d) => d, 1668 Err(e) => { 1669 output.push(format!("Invalid duration: {e}")); 1670 return 1671 } 1672 }; 1673 1674 let amount = String::from(parts[4]); 1675 if let Err(e) = f64::from_str(&amount) { 1676 output.push(format!("Invalid amount: {e}")); 1677 return 1678 } 1679 1680 let lock = drk.read().await; 1681 let token_id = match lock.get_token(String::from(parts[5])).await { 1682 Ok(t) => t, 1683 Err(e) => { 1684 output.push(format!("Invalid token ID: {e}")); 1685 return 1686 } 1687 }; 1688 1689 let rcpt = match PublicKey::from_str(parts[6]) { 1690 Ok(r) => r, 1691 Err(e) => { 1692 output.push(format!("Invalid recipient: {e}")); 1693 return 1694 } 1695 }; 1696 1697 let mut index = 7; 1698 let spend_hook = if index < parts.len() { 1699 match FuncId::from_str(parts[index]) { 1700 Ok(s) => Some(s), 1701 Err(e) => { 1702 output.push(format!("Invalid spend hook: {e}")); 1703 return 1704 } 1705 } 1706 } else { 1707 None 1708 }; 1709 index += 1; 1710 1711 let user_data = if index < parts.len() { 1712 let bytes = match bs58::decode(&parts[index]).into_vec() { 1713 Ok(b) => b, 1714 Err(e) => { 1715 output.push(format!("Invalid user data: {e}")); 1716 return 1717 } 1718 }; 1719 1720 let bytes: [u8; 32] = match bytes.try_into() { 1721 Ok(b) => b, 1722 Err(e) => { 1723 output.push(format!("Invalid user data: {e:?}")); 1724 return 1725 } 1726 }; 1727 1728 let elem: pallas::Base = match pallas::Base::from_repr(bytes).into() { 1729 Some(v) => v, 1730 None => { 1731 output.push(String::from("Invalid user data")); 1732 return 1733 } 1734 }; 1735 1736 Some(elem) 1737 } else { 1738 None 1739 }; 1740 1741 match drk 1742 .read() 1743 .await 1744 .dao_propose_transfer(parts[2], duration, &amount, token_id, rcpt, spend_hook, user_data) 1745 .await 1746 { 1747 Ok(proposal) => output.push(format!("Generated proposal: {}", proposal.bulla())), 1748 Err(e) => output.push(format!("Failed to create DAO transfer proposal: {e}")), 1749 } 1750 } 1751 1752 /// Auxiliary function to define the dao propose generic subcommand handling. 1753 async fn handle_dao_propose_generic(drk: &DrkPtr, parts: &[&str], output: &mut Vec<String>) { 1754 // Check correct subcommand structure 1755 if parts.len() != 4 && parts.len() != 5 { 1756 output.push(String::from("Malformed `dao proposal-generic` subcommand")); 1757 output.push(String::from("Usage: dao proposal-generic <name> <duration> [user-data]")); 1758 return 1759 } 1760 1761 let duration = match u64::from_str(parts[3]) { 1762 Ok(d) => d, 1763 Err(e) => { 1764 output.push(format!("Invalid duration: {e}")); 1765 return 1766 } 1767 }; 1768 1769 let user_data = if parts.len() == 5 { 1770 let bytes = match bs58::decode(&parts[4]).into_vec() { 1771 Ok(b) => b, 1772 Err(e) => { 1773 output.push(format!("Invalid user data: {e}")); 1774 return 1775 } 1776 }; 1777 1778 let bytes: [u8; 32] = match bytes.try_into() { 1779 Ok(b) => b, 1780 Err(e) => { 1781 output.push(format!("Invalid user data: {e:?}")); 1782 return 1783 } 1784 }; 1785 1786 let elem: pallas::Base = match pallas::Base::from_repr(bytes).into() { 1787 Some(v) => v, 1788 None => { 1789 output.push(String::from("Invalid user data")); 1790 return 1791 } 1792 }; 1793 1794 Some(elem) 1795 } else { 1796 None 1797 }; 1798 1799 match drk.read().await.dao_propose_generic(parts[2], duration, user_data).await { 1800 Ok(proposal) => output.push(format!("Generated proposal: {}", proposal.bulla())), 1801 Err(e) => output.push(format!("Failed to create DAO generic proposal: {e}")), 1802 } 1803 } 1804 1805 /// Auxiliary function to define the dao proposals subcommand handling. 1806 async fn handle_dao_proposals(drk: &DrkPtr, parts: &[&str], output: &mut Vec<String>) { 1807 // Check correct subcommand structure 1808 if parts.len() != 3 { 1809 output.push(String::from("Malformed `dao proposals` subcommand")); 1810 output.push(String::from("Usage: dao proposals <name>")); 1811 return 1812 } 1813 1814 match drk.read().await.get_dao_proposals(parts[2]).await { 1815 Ok(proposals) => { 1816 for (i, proposal) in proposals.iter().enumerate() { 1817 output.push(format!("{i}. {}", proposal.bulla())); 1818 } 1819 } 1820 Err(e) => output.push(format!("Failed to retrieve DAO proposals: {e}")), 1821 } 1822 } 1823 1824 /// Auxiliary function to define the dao proposal subcommand handling. 1825 async fn handle_dao_proposal(drk: &DrkPtr, parts: &[&str], output: &mut Vec<String>) { 1826 // Check correct subcommand structure 1827 if parts.len() != 3 && parts.len() != 4 { 1828 output.push(String::from("Malformed `dao proposal` subcommand")); 1829 output.push(String::from("Usage: dao proposal [--(export|mint-proposal)] <bulla>")); 1830 return 1831 } 1832 1833 let mut index = 2; 1834 let (export, mint_proposal) = if parts.len() == 4 { 1835 let flags = match parts[index] { 1836 "--export" => (true, false), 1837 "--mint-proposal" => (false, true), 1838 _ => { 1839 output.push(String::from("Malformed `dao proposal` subcommand")); 1840 output.push(String::from("Usage: dao proposal [--(export|mint-proposal)] <bulla>")); 1841 return 1842 } 1843 }; 1844 index += 1; 1845 flags 1846 } else { 1847 (false, false) 1848 }; 1849 1850 let bulla = match DaoProposalBulla::from_str(parts[index]) { 1851 Ok(b) => b, 1852 Err(e) => { 1853 output.push(format!("Invalid proposal bulla: {e}")); 1854 return 1855 } 1856 }; 1857 1858 let lock = drk.read().await; 1859 let proposal = match lock.get_dao_proposal_by_bulla(&bulla).await { 1860 Ok(p) => p, 1861 Err(e) => { 1862 output.push(format!("Failed to fetch DAO proposal: {e}")); 1863 return 1864 } 1865 }; 1866 1867 if export { 1868 // Retrieve the DAO 1869 let dao = match lock.get_dao_by_bulla(&proposal.proposal.dao_bulla).await { 1870 Ok(d) => d, 1871 Err(e) => { 1872 output.push(format!("Failed to fetch DAO: {e}")); 1873 return 1874 } 1875 }; 1876 1877 // Encypt the proposal 1878 let enc_note = 1879 AeadEncryptedNote::encrypt(&proposal, &dao.params.dao.proposals_public_key, &mut OsRng) 1880 .unwrap(); 1881 1882 // Export it to base64 1883 output.push(base64::encode(&serialize_async(&enc_note).await)); 1884 return 1885 } 1886 1887 if mint_proposal { 1888 // Identify proposal type by its auth calls 1889 for call in &proposal.proposal.auth_calls { 1890 // We only support transfer right now 1891 if call.function_code == DaoFunction::AuthMoneyTransfer as u8 { 1892 match lock.dao_transfer_proposal_tx(&proposal).await { 1893 Ok(tx) => output.push(base64::encode(&serialize_async(&tx).await)), 1894 Err(e) => output.push(format!("Failed to create DAO transfer proposal: {e}")), 1895 } 1896 return 1897 } 1898 } 1899 1900 // If proposal has no auth calls, we consider it a generic one 1901 if proposal.proposal.auth_calls.is_empty() { 1902 match lock.dao_generic_proposal_tx(&proposal).await { 1903 Ok(tx) => output.push(base64::encode(&serialize_async(&tx).await)), 1904 Err(e) => output.push(format!("Failed to create DAO generic proposal: {e}")), 1905 } 1906 return 1907 } 1908 1909 output.push(String::from("Unsuported DAO proposal")); 1910 return 1911 } 1912 1913 output.push(format!("{proposal}")); 1914 1915 let mut contract_calls = "\nInvoked contracts:\n".to_string(); 1916 for call in proposal.proposal.auth_calls { 1917 contract_calls.push_str(&format!( 1918 "\tContract: {}\n\tFunction: {}\n\tData: ", 1919 call.contract_id, call.function_code 1920 )); 1921 1922 if call.auth_data.is_empty() { 1923 contract_calls.push_str("-\n"); 1924 continue; 1925 } 1926 1927 if call.function_code == DaoFunction::AuthMoneyTransfer as u8 { 1928 // We know that the plaintext data live in the data plaintext vec 1929 if proposal.data.is_none() { 1930 contract_calls.push_str("-\n"); 1931 continue; 1932 } 1933 let coin: CoinAttributes = match deserialize_async(proposal.data.as_ref().unwrap()) 1934 .await 1935 { 1936 Ok(c) => c, 1937 Err(e) => { 1938 output.push(format!("Failed to deserialize transfer proposal coin data: {e}")); 1939 return 1940 } 1941 }; 1942 let spend_hook = if coin.spend_hook == FuncId::none() { 1943 "-".to_string() 1944 } else { 1945 format!("{}", coin.spend_hook) 1946 }; 1947 1948 let user_data = if coin.user_data == pallas::Base::ZERO { 1949 "-".to_string() 1950 } else { 1951 format!("{:?}", coin.user_data) 1952 }; 1953 1954 contract_calls.push_str(&format!( 1955 "\n\t\t{}: {}\n\t\t{}: {} ({})\n\t\t{}: {}\n\t\t{}: {}\n\t\t{}: {}\n\t\t{}: {}\n\n", 1956 "Recipient", 1957 coin.public_key, 1958 "Amount", 1959 coin.value, 1960 encode_base10(coin.value, BALANCE_BASE10_DECIMALS), 1961 "Token", 1962 coin.token_id, 1963 "Spend hook", 1964 spend_hook, 1965 "User data", 1966 user_data, 1967 "Blind", 1968 coin.blind 1969 )); 1970 } 1971 } 1972 1973 output.push(contract_calls); 1974 1975 let votes = match lock.get_dao_proposal_votes(&bulla).await { 1976 Ok(v) => v, 1977 Err(e) => { 1978 output.push(format!("Failed to fetch DAO proposal votes: {e}")); 1979 return 1980 } 1981 }; 1982 let mut total_yes_vote_value = 0; 1983 let mut total_no_vote_value = 0; 1984 let mut total_all_vote_value = 0; 1985 let mut table = Table::new(); 1986 table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); 1987 table.set_titles(row!["Transaction", "Tokens", "Vote"]); 1988 for vote in votes { 1989 let vote_option = if vote.vote_option { 1990 total_yes_vote_value += vote.all_vote_value; 1991 "Yes" 1992 } else { 1993 total_no_vote_value += vote.all_vote_value; 1994 "No" 1995 }; 1996 total_all_vote_value += vote.all_vote_value; 1997 1998 table.add_row(row![ 1999 vote.tx_hash, 2000 encode_base10(vote.all_vote_value, BALANCE_BASE10_DECIMALS), 2001 vote_option 2002 ]); 2003 } 2004 2005 let outcome = if table.is_empty() { 2006 output.push(String::from("Votes: No votes found")); 2007 "Unknown" 2008 } else { 2009 output.push(String::from("Votes:")); 2010 output.push(format!("{table}")); 2011 output.push(format!( 2012 "Total tokens votes: {}", 2013 encode_base10(total_all_vote_value, BALANCE_BASE10_DECIMALS) 2014 )); 2015 let approval_ratio = (total_yes_vote_value as f64 * 100.0) / total_all_vote_value as f64; 2016 output.push(format!( 2017 "Total tokens Yes votes: {} ({approval_ratio:.2}%)", 2018 encode_base10(total_yes_vote_value, BALANCE_BASE10_DECIMALS) 2019 )); 2020 output.push(format!( 2021 "Total tokens No votes: {} ({:.2}%)", 2022 encode_base10(total_no_vote_value, BALANCE_BASE10_DECIMALS), 2023 (total_no_vote_value as f64 * 100.0) / total_all_vote_value as f64 2024 )); 2025 2026 let dao = match lock.get_dao_by_bulla(&proposal.proposal.dao_bulla).await { 2027 Ok(d) => d, 2028 Err(e) => { 2029 output.push(format!("Failed to fetch DAO: {e}")); 2030 return 2031 } 2032 }; 2033 if total_all_vote_value >= dao.params.dao.quorum && 2034 approval_ratio >= 2035 (dao.params.dao.approval_ratio_quot / dao.params.dao.approval_ratio_base) 2036 as f64 2037 { 2038 "Approved" 2039 } else { 2040 "Rejected" 2041 } 2042 }; 2043 2044 if let Some(exec_tx_hash) = proposal.exec_tx_hash { 2045 output.push(format!("Proposal was executed on transaction: {exec_tx_hash}")); 2046 return 2047 } 2048 2049 // Retrieve next block height and current block time target, 2050 // to compute their window. 2051 let next_block_height = match lock.get_next_block_height().await { 2052 Ok(n) => n, 2053 Err(e) => { 2054 output.push(format!("Failed to fetch next block height: {e}")); 2055 return 2056 } 2057 }; 2058 let block_target = match lock.get_block_target().await { 2059 Ok(b) => b, 2060 Err(e) => { 2061 output.push(format!("Failed to fetch block target: {e}")); 2062 return 2063 } 2064 }; 2065 let current_window = blockwindow(next_block_height, block_target); 2066 let end_time = proposal.proposal.creation_blockwindow + proposal.proposal.duration_blockwindows; 2067 let (voting_status, proposal_status_message) = if current_window < end_time { 2068 ("Ongoing", format!("Current proposal outcome: {outcome}")) 2069 } else { 2070 ("Concluded", format!("Proposal outcome: {outcome}")) 2071 }; 2072 output.push(format!("Voting status: {voting_status}")); 2073 output.push(proposal_status_message); 2074 } 2075 2076 /// Auxiliary function to define the dao proposal import subcommand handling. 2077 async fn handle_dao_proposal_import( 2078 drk: &DrkPtr, 2079 parts: &[&str], 2080 input: &[String], 2081 output: &mut Vec<String>, 2082 ) { 2083 // Check correct subcommand structure 2084 if parts.len() != 2 { 2085 output.push(String::from("Malformed `dao proposal-import` subcommand")); 2086 output.push(String::from("Usage: dao proposal-import")); 2087 return 2088 } 2089 2090 // Parse line from input or fallback to stdin if its empty 2091 let buf = match input.len() { 2092 0 => { 2093 let mut buf = String::new(); 2094 if let Err(e) = stdin().read_to_string(&mut buf) { 2095 output.push(format!("Failed to read from stdin: {e}")); 2096 return 2097 }; 2098 buf 2099 } 2100 1 => input[0].clone(), 2101 _ => { 2102 output.push(String::from("Multiline input provided")); 2103 return 2104 } 2105 }; 2106 2107 let Some(bytes) = base64::decode(buf.trim()) else { 2108 output.push(String::from("Failed to decode encrypted proposal data")); 2109 return 2110 }; 2111 2112 let encrypted_proposal: AeadEncryptedNote = match deserialize_async(&bytes).await { 2113 Ok(e) => e, 2114 Err(e) => { 2115 output.push(format!("Failed to deserialize encrypted proposal data: {e}")); 2116 return 2117 } 2118 }; 2119 2120 let lock = drk.read().await; 2121 let daos = match lock.get_daos().await { 2122 Ok(d) => d, 2123 Err(e) => { 2124 output.push(format!("Failed to retrieve DAOs: {e}")); 2125 return 2126 } 2127 }; 2128 2129 for dao in &daos { 2130 // Check if we have the proposals key 2131 let Some(proposals_secret_key) = dao.params.proposals_secret_key else { continue }; 2132 2133 // Try to decrypt the proposal 2134 let Ok(proposal) = encrypted_proposal.decrypt::<ProposalRecord>(&proposals_secret_key) 2135 else { 2136 continue 2137 }; 2138 2139 let proposal = match lock.get_dao_proposal_by_bulla(&proposal.bulla()).await { 2140 Ok(p) => { 2141 let mut our_proposal = p; 2142 our_proposal.data = proposal.data; 2143 our_proposal 2144 } 2145 Err(_) => proposal, 2146 }; 2147 2148 if let Err(e) = lock.put_dao_proposal(&proposal).await { 2149 output.push(format!("Failed to put DAO proposal: {e}")); 2150 } 2151 return 2152 } 2153 2154 output.push(String::from("Couldn't decrypt the proposal with out DAO keys")); 2155 } 2156 2157 /// Auxiliary function to define the dao vote subcommand handling. 2158 async fn handle_dao_vote(drk: &DrkPtr, parts: &[&str], output: &mut Vec<String>) { 2159 // Check correct subcommand structure 2160 if parts.len() != 4 && parts.len() != 5 { 2161 output.push(String::from("Malformed `dao vote` subcommand")); 2162 output.push(String::from("Usage: dao vote <bulla> <vote> [vote-weight]")); 2163 return 2164 } 2165 2166 let bulla = match DaoProposalBulla::from_str(parts[2]) { 2167 Ok(b) => b, 2168 Err(e) => { 2169 output.push(format!("Invalid proposal bulla: {e}")); 2170 return 2171 } 2172 }; 2173 2174 let vote = match u8::from_str(parts[3]) { 2175 Ok(v) => { 2176 if v > 1 { 2177 output.push(String::from("Vote can be either 0 (NO) or 1 (YES)")); 2178 return 2179 } 2180 v != 0 2181 } 2182 Err(e) => { 2183 output.push(format!("Invalid vote: {e}")); 2184 return 2185 } 2186 }; 2187 2188 let weight = if parts.len() == 5 { 2189 if let Err(e) = f64::from_str(parts[4]) { 2190 output.push(format!("Invalid vote weight: {e}")); 2191 return 2192 } 2193 match decode_base10(parts[4], BALANCE_BASE10_DECIMALS, true) { 2194 Ok(w) => Some(w), 2195 Err(e) => { 2196 output.push(format!("Error while parsing vote weight: {e}")); 2197 return 2198 } 2199 } 2200 } else { 2201 None 2202 }; 2203 2204 match drk.read().await.dao_vote(&bulla, vote, weight).await { 2205 Ok(tx) => output.push(base64::encode(&serialize_async(&tx).await)), 2206 Err(e) => output.push(format!("Failed to create DAO Vote transaction: {e}")), 2207 } 2208 } 2209 2210 /// Auxiliary function to define the dao exec subcommand handling. 2211 async fn handle_dao_exec(drk: &DrkPtr, parts: &[&str], output: &mut Vec<String>) { 2212 // Check correct subcommand structure 2213 if parts.len() != 3 && parts.len() != 4 { 2214 output.push(String::from("Malformed `dao exec` subcommand")); 2215 output.push(String::from("Usage: dao exec [--early] <bulla>")); 2216 return 2217 } 2218 2219 let mut index = 2; 2220 let mut early = false; 2221 if parts[index] == "--early" { 2222 early = true; 2223 index += 1; 2224 } 2225 2226 let bulla = match DaoProposalBulla::from_str(parts[index]) { 2227 Ok(b) => b, 2228 Err(e) => { 2229 output.push(format!("Invalid proposal bulla: {e}")); 2230 return 2231 } 2232 }; 2233 2234 let lock = drk.read().await; 2235 let proposal = match lock.get_dao_proposal_by_bulla(&bulla).await { 2236 Ok(p) => p, 2237 Err(e) => { 2238 output.push(format!("Failed to fetch DAO proposal: {e}")); 2239 return 2240 } 2241 }; 2242 2243 // Identify proposal type by its auth calls 2244 for call in &proposal.proposal.auth_calls { 2245 // We only support transfer right now 2246 if call.function_code == DaoFunction::AuthMoneyTransfer as u8 { 2247 match lock.dao_exec_transfer(&proposal, early).await { 2248 Ok(tx) => output.push(base64::encode(&serialize_async(&tx).await)), 2249 Err(e) => output.push(format!("Failed to execute DAO transfer proposal: {e}")), 2250 }; 2251 return 2252 } 2253 } 2254 2255 // If proposal has no auth calls, we consider it a generic one 2256 if proposal.proposal.auth_calls.is_empty() { 2257 match lock.dao_exec_generic(&proposal, early).await { 2258 Ok(tx) => output.push(base64::encode(&serialize_async(&tx).await)), 2259 Err(e) => output.push(format!("Failed to execute DAO generic proposal: {e}")), 2260 }; 2261 return 2262 } 2263 2264 output.push(String::from("Unsuported DAO proposal")); 2265 } 2266 2267 /// Auxiliary function to define the dao spent hook subcommand handling. 2268 async fn handle_dao_spend_hook(parts: &[&str], output: &mut Vec<String>) { 2269 // Check correct subcommand structure 2270 if parts.len() != 2 { 2271 output.push(String::from("Malformed `dao spent-hook` subcommand")); 2272 output.push(String::from("Usage: dao spent-hook")); 2273 return 2274 } 2275 2276 let spend_hook = 2277 FuncRef { contract_id: *DAO_CONTRACT_ID, func_code: DaoFunction::Exec as u8 }.to_func_id(); 2278 output.push(format!("{spend_hook}")); 2279 } 2280 2281 /// Auxiliary function to define the attach fee command handling. 2282 async fn handle_attach_fee(drk: &DrkPtr, input: &[String], output: &mut Vec<String>) { 2283 let mut tx = match parse_tx_from_input(input).await { 2284 Ok(t) => t, 2285 Err(e) => { 2286 output.push(format!("Error while parsing transaction: {e}")); 2287 return 2288 } 2289 }; 2290 2291 match drk.read().await.attach_fee(&mut tx).await { 2292 Ok(_) => output.push(base64::encode(&serialize_async(&tx).await)), 2293 Err(e) => output.push(format!("Failed to attach the fee call to the transaction: {e}")), 2294 } 2295 } 2296 2297 /// Auxiliary function to define the inspect command handling. 2298 async fn handle_inspect(input: &[String], output: &mut Vec<String>) { 2299 match parse_tx_from_input(input).await { 2300 Ok(tx) => output.push(format!("{tx:#?}")), 2301 Err(e) => output.push(format!("Error while parsing transaction: {e}")), 2302 } 2303 } 2304 2305 /// Auxiliary function to define the broadcast command handling. 2306 async fn handle_broadcast(drk: &DrkPtr, input: &[String], output: &mut Vec<String>) { 2307 let tx = match parse_tx_from_input(input).await { 2308 Ok(t) => t, 2309 Err(e) => { 2310 output.push(format!("Error while parsing transaction: {e}")); 2311 return 2312 } 2313 }; 2314 2315 let lock = drk.read().await; 2316 if let Err(e) = lock.simulate_tx(&tx).await { 2317 output.push(format!("Failed to simulate tx: {e}")); 2318 return 2319 }; 2320 2321 if let Err(e) = lock.mark_tx_spend(&tx, output).await { 2322 output.push(format!("Failed to mark transaction coins as spent: {e}")); 2323 return 2324 }; 2325 2326 match lock.broadcast_tx(&tx, output).await { 2327 Ok(txid) => output.push(format!("Transaction ID: {txid}")), 2328 Err(e) => output.push(format!("Failed to broadcast transaction: {e}")), 2329 } 2330 } 2331 2332 /// Auxiliary function to define the subscribe command handling. 2333 async fn handle_subscribe( 2334 drk: &DrkPtr, 2335 endpoint: &Url, 2336 subscription_active: &mut bool, 2337 subscription_tasks: &[StoppableTaskPtr; 2], 2338 shell_sender: &Sender<Vec<String>>, 2339 ex: &ExecutorPtr, 2340 ) { 2341 // Kill zombie tasks if they failed 2342 subscription_tasks[0].stop_nowait(); 2343 subscription_tasks[1].stop_nowait(); 2344 *subscription_active = true; 2345 2346 // Start the subscription task 2347 let drk_ = drk.clone(); 2348 let rpc_task_ = subscription_tasks[1].clone(); 2349 let shell_sender_ = shell_sender.clone(); 2350 let endpoint_ = endpoint.clone(); 2351 let ex_ = ex.clone(); 2352 subscription_tasks[0].clone().start( 2353 async move { subscribe_blocks(&drk_, rpc_task_, shell_sender_, endpoint_, &ex_).await }, 2354 |res| async { 2355 match res { 2356 Ok(()) | Err(Error::DetachedTaskStopped) => { /* Do nothing */ } 2357 Err(e) => println!("Failed starting subscription task: {e}"), 2358 } 2359 }, 2360 Error::DetachedTaskStopped, 2361 ex.clone(), 2362 ); 2363 } 2364 2365 /// Auxiliary function to define the unsubscribe command handling. 2366 async fn handle_unsubscribe( 2367 subscription_active: &mut bool, 2368 subscription_tasks: &[StoppableTaskPtr; 2], 2369 ) { 2370 subscription_tasks[0].stop_nowait(); 2371 subscription_tasks[1].stop_nowait(); 2372 *subscription_active = false; 2373 } 2374 2375 /// Auxiliary function to define the scan command handling. 2376 async fn handle_scan( 2377 drk: &DrkPtr, 2378 subscription_active: &bool, 2379 parts: &[&str], 2380 output: &mut Vec<String>, 2381 print: &bool, 2382 ) { 2383 if *subscription_active { 2384 append_or_print(output, None, print, vec![String::from("Subscription is already active!")]) 2385 .await; 2386 return 2387 } 2388 2389 // Check correct command structure 2390 if parts.len() != 1 && parts.len() != 3 { 2391 append_or_print(output, None, print, vec![String::from("Malformed `scan` command")]).await; 2392 return 2393 } 2394 2395 // Check if reset was requested 2396 let lock = drk.read().await; 2397 if parts.len() == 3 { 2398 if parts[1] != "--reset" { 2399 append_or_print( 2400 output, 2401 None, 2402 print, 2403 vec![ 2404 String::from("Malformed `scan` command"), 2405 String::from("Usage: scan --reset <height>"), 2406 ], 2407 ) 2408 .await; 2409 return 2410 } 2411 2412 let height = match u32::from_str(parts[2]) { 2413 Ok(h) => h, 2414 Err(e) => { 2415 append_or_print(output, None, print, vec![format!("Invalid reset height: {e}")]) 2416 .await; 2417 return 2418 } 2419 }; 2420 2421 let mut buf = vec![]; 2422 if let Err(e) = lock.reset_to_height(height, &mut buf).await { 2423 buf.push(format!("Failed during wallet reset: {e}")); 2424 append_or_print(output, None, print, buf).await; 2425 return 2426 } 2427 append_or_print(output, None, print, buf).await; 2428 } 2429 2430 if let Err(e) = lock.scan_blocks(output, None, print).await { 2431 append_or_print(output, None, print, vec![format!("Failed during scanning: {e}")]).await; 2432 return 2433 } 2434 append_or_print(output, None, print, vec![String::from("Finished scanning blockchain")]).await; 2435 } 2436 2437 /// Auxiliary function to define the explorer command handling. 2438 async fn handle_explorer(drk: &DrkPtr, parts: &[&str], input: &[String], output: &mut Vec<String>) { 2439 // Check correct command structure 2440 if parts.len() < 2 { 2441 output.push(String::from("Malformed `explorer` command")); 2442 output.push(String::from( 2443 "Usage: explorer (fetch-tx|simulate-tx|txs-history|clear-reverted|scanned-blocks)", 2444 )); 2445 return 2446 } 2447 2448 // Handle subcommand 2449 match parts[1] { 2450 "fetch-tx" => handle_explorer_fetch_tx(drk, parts, output).await, 2451 "simulate-tx" => handle_explorer_simulate_tx(drk, parts, input, output).await, 2452 "txs-history" => handle_explorer_txs_history(drk, parts, output).await, 2453 "clear-reverted" => handle_explorer_clear_reverted(drk, parts, output).await, 2454 "scanned-blocks" => handle_explorer_scanned_blocks(drk, parts, output).await, 2455 _ => { 2456 output.push(format!("Unrecognized explorer subcommand: {}", parts[1])); 2457 output.push(String::from( 2458 "Usage: explorer (fetch-tx|simulate-tx|txs-history|clear-reverted|scanned-blocks)", 2459 )); 2460 } 2461 } 2462 } 2463 2464 /// Auxiliary function to define the explorer fetch transaction subcommand handling. 2465 async fn handle_explorer_fetch_tx(drk: &DrkPtr, parts: &[&str], output: &mut Vec<String>) { 2466 // Check correct subcommand structure 2467 if parts.len() != 3 && parts.len() != 4 { 2468 output.push(String::from("Malformed `explorer fetch-tx` subcommand")); 2469 output.push(String::from("Usage: explorer fetch-tx [--encode] <tx-hash>")); 2470 return 2471 } 2472 2473 let mut index = 2; 2474 let mut encode = false; 2475 if parts[index] == "--encode" { 2476 encode = true; 2477 index += 1; 2478 } 2479 2480 let hash = match blake3::Hash::from_hex(parts[index]) { 2481 Ok(h) => h, 2482 Err(e) => { 2483 output.push(format!("Invalid transaction hash: {e}")); 2484 return 2485 } 2486 }; 2487 let tx_hash = TransactionHash(*hash.as_bytes()); 2488 2489 let tx = match drk.read().await.get_tx(&tx_hash).await { 2490 Ok(tx) => tx, 2491 Err(e) => { 2492 output.push(format!("Failed to fetch transaction: {e}")); 2493 return 2494 } 2495 }; 2496 2497 let Some(tx) = tx else { 2498 output.push(String::from("Transaction was not found")); 2499 return 2500 }; 2501 2502 // Make sure the tx is correct 2503 if tx.hash() != tx_hash { 2504 output.push(format!("Transaction hash missmatch: {tx_hash} - {}", tx.hash())); 2505 return 2506 } 2507 2508 if encode { 2509 output.push(base64::encode(&serialize_async(&tx).await)); 2510 return 2511 } 2512 2513 output.push(format!("Transaction ID: {tx_hash}")); 2514 output.push(format!("{tx:?}")); 2515 } 2516 2517 /// Auxiliary function to define the explorer simulate transaction subcommand handling. 2518 async fn handle_explorer_simulate_tx( 2519 drk: &DrkPtr, 2520 parts: &[&str], 2521 input: &[String], 2522 output: &mut Vec<String>, 2523 ) { 2524 // Check correct subcommand structure 2525 if parts.len() != 2 { 2526 output.push(String::from("Malformed `explorer simulate-tx` subcommand")); 2527 output.push(String::from("Usage: explorer simulate-tx")); 2528 return 2529 } 2530 2531 let tx = match parse_tx_from_input(input).await { 2532 Ok(t) => t, 2533 Err(e) => { 2534 output.push(format!("Error while parsing transaction: {e}")); 2535 return 2536 } 2537 }; 2538 2539 match drk.read().await.simulate_tx(&tx).await { 2540 Ok(is_valid) => { 2541 output.push(format!("Transaction ID: {}", tx.hash())); 2542 output.push(format!("State: {}", if is_valid { "valid" } else { "invalid" })); 2543 } 2544 Err(e) => output.push(format!("Failed to simulate tx: {e}")), 2545 } 2546 } 2547 2548 /// Auxiliary function to define the explorer transactions history subcommand handling. 2549 async fn handle_explorer_txs_history(drk: &DrkPtr, parts: &[&str], output: &mut Vec<String>) { 2550 // Check correct command structure 2551 if parts.len() < 2 || parts.len() > 4 { 2552 output.push(String::from("Malformed `explorer txs-history` command")); 2553 output.push(String::from("Usage: explorer txs-history [--encode] [tx-hash]")); 2554 return 2555 } 2556 2557 let lock = drk.read().await; 2558 if parts.len() > 2 { 2559 let mut index = 2; 2560 let mut encode = false; 2561 if parts[index] == "--encode" { 2562 encode = true; 2563 index += 1; 2564 } 2565 2566 let (tx_hash, status, block_height, tx) = 2567 match lock.get_tx_history_record(parts[index]).await { 2568 Ok(i) => i, 2569 Err(e) => { 2570 output.push(format!("Failed to fetch transaction: {e}")); 2571 return 2572 } 2573 }; 2574 2575 if encode { 2576 output.push(base64::encode(&serialize_async(&tx).await)); 2577 return 2578 } 2579 2580 output.push(format!("Transaction ID: {tx_hash}")); 2581 output.push(format!("Status: {status}")); 2582 match block_height { 2583 Some(block_height) => output.push(format!("Block height: {block_height}")), 2584 None => output.push(String::from("Block height: -")), 2585 } 2586 output.push(format!("{tx:?}")); 2587 return 2588 } 2589 2590 let map = match lock.get_txs_history() { 2591 Ok(m) => m, 2592 Err(e) => { 2593 output.push(format!("Failed to retrieve transactions history records: {e}")); 2594 return 2595 } 2596 }; 2597 2598 // Create a prettytable with the new data: 2599 let mut table = Table::new(); 2600 table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); 2601 table.set_titles(row!["Transaction Hash", "Status", "Block Height"]); 2602 for (txs_hash, status, block_height) in map.iter() { 2603 let block_height = match block_height { 2604 Some(block_height) => block_height.to_string(), 2605 None => String::from("-"), 2606 }; 2607 table.add_row(row![txs_hash, status, block_height]); 2608 } 2609 2610 if table.is_empty() { 2611 output.push(String::from("No transactions found")); 2612 } else { 2613 output.push(format!("{table}")); 2614 } 2615 } 2616 2617 /// Auxiliary function to define the explorer clear reverted subcommand handling. 2618 async fn handle_explorer_clear_reverted(drk: &DrkPtr, parts: &[&str], output: &mut Vec<String>) { 2619 // Check correct subcommand structure 2620 if parts.len() != 2 { 2621 output.push(String::from("Malformed `explorer clear-reverted` subcommand")); 2622 output.push(String::from("Usage: explorer clear-reverted")); 2623 return 2624 } 2625 2626 if let Err(e) = drk.read().await.remove_reverted_txs(output) { 2627 output.push(format!("Failed to remove reverted transactions: {e}")); 2628 } 2629 } 2630 2631 /// Auxiliary function to define the explorer scanned blocks subcommand handling. 2632 async fn handle_explorer_scanned_blocks(drk: &DrkPtr, parts: &[&str], output: &mut Vec<String>) { 2633 // Check correct subcommand structure 2634 if parts.len() != 2 && parts.len() != 3 { 2635 output.push(String::from("Malformed `explorer scanned-blocks` subcommand")); 2636 output.push(String::from("Usage: explorer scanned-blocks [height]")); 2637 return 2638 } 2639 2640 let lock = drk.read().await; 2641 if parts.len() == 3 { 2642 let height = match u32::from_str(parts[2]) { 2643 Ok(d) => d, 2644 Err(e) => { 2645 output.push(format!("Invalid height: {e}")); 2646 return 2647 } 2648 }; 2649 2650 match lock.get_scanned_block_hash(&height) { 2651 Ok(hash) => { 2652 output.push(format!("Height: {height}")); 2653 output.push(format!("Hash: {hash}")); 2654 } 2655 Err(e) => output.push(format!("Failed to retrieve scanned block record: {e}")), 2656 }; 2657 return 2658 } 2659 2660 let map = match lock.get_scanned_block_records() { 2661 Ok(m) => m, 2662 Err(e) => { 2663 output.push(format!("Failed to retrieve scanned blocks records: {e}")); 2664 return 2665 } 2666 }; 2667 2668 // Create a prettytable with the new data: 2669 let mut table = Table::new(); 2670 table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); 2671 table.set_titles(row!["Height", "Hash"]); 2672 for (height, hash) in map.iter() { 2673 table.add_row(row![height, hash]); 2674 } 2675 2676 if table.is_empty() { 2677 output.push(String::from("No scanned blocks records found")); 2678 } else { 2679 output.push(format!("{table}")); 2680 } 2681 } 2682 2683 /// Auxiliary function to define the alias command handling. 2684 async fn handle_alias(drk: &DrkPtr, parts: &[&str], output: &mut Vec<String>) { 2685 // Check correct command structure 2686 if parts.len() < 2 { 2687 output.push(String::from("Malformed `alias` command")); 2688 output.push(String::from("Usage: alias (add|show|remove)")); 2689 return 2690 } 2691 2692 // Handle subcommand 2693 match parts[1] { 2694 "add" => handle_alias_add(drk, parts, output).await, 2695 "show" => handle_alias_show(drk, parts, output).await, 2696 "remove" => handle_alias_remove(drk, parts, output).await, 2697 _ => { 2698 output.push(format!("Unrecognized alias subcommand: {}", parts[1])); 2699 output.push(String::from("Usage: alias (add|show|remove)")); 2700 } 2701 } 2702 } 2703 2704 /// Auxiliary function to define the alias add subcommand handling. 2705 async fn handle_alias_add(drk: &DrkPtr, parts: &[&str], output: &mut Vec<String>) { 2706 // Check correct subcommand structure 2707 if parts.len() != 4 { 2708 output.push(String::from("Malformed `alias add` subcommand")); 2709 output.push(String::from("Usage: alias add <alias> <token>")); 2710 return 2711 } 2712 2713 if parts[2].len() > 5 { 2714 output.push(String::from("Error: Alias exceeds 5 characters")); 2715 return 2716 } 2717 2718 let token_id = match TokenId::from_str(parts[3]) { 2719 Ok(t) => t, 2720 Err(e) => { 2721 output.push(format!("Invalid Token ID: {e}")); 2722 return 2723 } 2724 }; 2725 2726 if let Err(e) = drk.read().await.add_alias(String::from(parts[2]), token_id, output).await { 2727 output.push(format!("Failed to add alias: {e}")); 2728 } 2729 } 2730 2731 /// Auxiliary function to define the alias show subcommand handling. 2732 async fn handle_alias_show(drk: &DrkPtr, parts: &[&str], output: &mut Vec<String>) { 2733 // Check correct command structure 2734 if parts.len() != 2 && parts.len() != 4 && parts.len() != 6 { 2735 output.push(String::from("Malformed `alias show` command")); 2736 output.push(String::from("Usage: alias show [-a, --alias <alias>] [-t, --token <token>]")); 2737 return 2738 } 2739 2740 let mut alias = None; 2741 let mut token_id = None; 2742 if parts.len() > 2 { 2743 let mut index = 2; 2744 if parts[index] == "-a" || parts[index] == "--alias" { 2745 alias = Some(String::from(parts[index + 1])); 2746 index += 2; 2747 } 2748 2749 if index < parts.len() && (parts[index] == "-t" || parts[index] == "--token") { 2750 match TokenId::from_str(parts[index + 1]) { 2751 Ok(t) => token_id = Some(t), 2752 Err(e) => { 2753 output.push(format!("Invalid Token ID: {e}")); 2754 return 2755 } 2756 }; 2757 index += 2; 2758 } 2759 2760 // Check alias again in case it was after token 2761 if index < parts.len() && (parts[index] == "-a" || parts[index] == "--alias") { 2762 alias = Some(String::from(parts[index + 1])); 2763 } 2764 } 2765 2766 let map = match drk.read().await.get_aliases(alias, token_id).await { 2767 Ok(m) => m, 2768 Err(e) => { 2769 output.push(format!("Failed to fetch aliases map: {e}")); 2770 return 2771 } 2772 }; 2773 2774 // Create a prettytable with the new data: 2775 let mut table = Table::new(); 2776 table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); 2777 table.set_titles(row!["Alias", "Token ID"]); 2778 for (alias, token_id) in map.iter() { 2779 table.add_row(row![alias, token_id]); 2780 } 2781 2782 if table.is_empty() { 2783 output.push(String::from("No aliases found")); 2784 } else { 2785 output.push(format!("{table}")); 2786 } 2787 } 2788 2789 /// Auxiliary function to define the alias remove subcommand handling. 2790 async fn handle_alias_remove(drk: &DrkPtr, parts: &[&str], output: &mut Vec<String>) { 2791 // Check correct subcommand structure 2792 if parts.len() != 3 { 2793 output.push(String::from("Malformed `alias remove` subcommand")); 2794 output.push(String::from("Usage: alias remove <alias>")); 2795 return 2796 } 2797 2798 if let Err(e) = drk.read().await.remove_alias(String::from(parts[2]), output).await { 2799 output.push(format!("Failed to remove alias: {e}")); 2800 } 2801 } 2802 2803 /// Auxiliary function to define the token command handling. 2804 async fn handle_token(drk: &DrkPtr, parts: &[&str], output: &mut Vec<String>) { 2805 // Check correct command structure 2806 if parts.len() < 2 { 2807 output.push(String::from("Malformed `token` command")); 2808 output.push(String::from("Usage: token (import|generate-mint|list|mint|freeze)")); 2809 return 2810 } 2811 2812 // Handle subcommand 2813 match parts[1] { 2814 "import" => handle_token_import(drk, parts, output).await, 2815 "generate-mint" => handle_token_generate_mint(drk, parts, output).await, 2816 "list" => handle_token_list(drk, parts, output).await, 2817 "mint" => handle_token_mint(drk, parts, output).await, 2818 "freeze" => handle_token_freeze(drk, parts, output).await, 2819 _ => { 2820 output.push(format!("Unrecognized token subcommand: {}", parts[1])); 2821 output.push(String::from("Usage: token (import|generate-mint|list|mint|freeze)")); 2822 } 2823 } 2824 } 2825 2826 /// Auxiliary function to define the token import subcommand handling. 2827 async fn handle_token_import(drk: &DrkPtr, parts: &[&str], output: &mut Vec<String>) { 2828 // Check correct subcommand structure 2829 if parts.len() != 4 { 2830 output.push(String::from("Malformed `token import` subcommand")); 2831 output.push(String::from("Usage: token import <secret-key> <token-blind>")); 2832 return 2833 } 2834 2835 let mint_authority = match SecretKey::from_str(parts[2]) { 2836 Ok(ma) => ma, 2837 Err(e) => { 2838 output.push(format!("Invalid mint authority: {e}")); 2839 return 2840 } 2841 }; 2842 2843 let token_blind = match BaseBlind::from_str(parts[3]) { 2844 Ok(tb) => tb, 2845 Err(e) => { 2846 output.push(format!("Invalid token blind: {e}")); 2847 return 2848 } 2849 }; 2850 2851 match drk.read().await.import_mint_authority(mint_authority, token_blind).await { 2852 Ok(token_id) => { 2853 output.push(format!("Successfully imported mint authority for token ID: {token_id}")) 2854 } 2855 Err(e) => output.push(format!("Failed to import mint authority: {e}")), 2856 } 2857 } 2858 2859 /// Auxiliary function to define the token generate mint subcommand handling. 2860 async fn handle_token_generate_mint(drk: &DrkPtr, parts: &[&str], output: &mut Vec<String>) { 2861 // Check correct subcommand structure 2862 if parts.len() != 2 { 2863 output.push(String::from("Malformed `token generate-mint` subcommand")); 2864 output.push(String::from("Usage: token generate-mint")); 2865 return 2866 } 2867 2868 let mint_authority = SecretKey::random(&mut OsRng); 2869 let token_blind = BaseBlind::random(&mut OsRng); 2870 match drk.read().await.import_mint_authority(mint_authority, token_blind).await { 2871 Ok(token_id) => { 2872 output.push(format!("Successfully imported mint authority for token ID: {token_id}")) 2873 } 2874 Err(e) => output.push(format!("Failed to import mint authority: {e}")), 2875 } 2876 } 2877 2878 /// Auxiliary function to define the token list subcommand handling. 2879 async fn handle_token_list(drk: &DrkPtr, parts: &[&str], output: &mut Vec<String>) { 2880 // Check correct subcommand structure 2881 if parts.len() != 2 { 2882 output.push(String::from("Malformed `token list` subcommand")); 2883 output.push(String::from("Usage: token list")); 2884 return 2885 } 2886 2887 let lock = drk.read().await; 2888 let tokens = match lock.get_mint_authorities().await { 2889 Ok(m) => m, 2890 Err(e) => { 2891 output.push(format!("Failed to fetch mint authorities: {e}")); 2892 return 2893 } 2894 }; 2895 2896 let aliases_map = match lock.get_aliases_mapped_by_token().await { 2897 Ok(m) => m, 2898 Err(e) => { 2899 output.push(format!("Failed to fetch aliases map: {e}")); 2900 return 2901 } 2902 }; 2903 2904 let mut table = Table::new(); 2905 table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); 2906 table.set_titles(row![ 2907 "Token ID", 2908 "Aliases", 2909 "Mint Authority", 2910 "Token Blind", 2911 "Frozen", 2912 "Freeze Height" 2913 ]); 2914 2915 for (token_id, authority, blind, frozen, freeze_height) in tokens { 2916 let aliases = match aliases_map.get(&token_id.to_string()) { 2917 Some(a) => a, 2918 None => "-", 2919 }; 2920 2921 let freeze_height = match freeze_height { 2922 Some(freeze_height) => freeze_height.to_string(), 2923 None => String::from("-"), 2924 }; 2925 2926 table.add_row(row![token_id, aliases, authority, blind, frozen, freeze_height]); 2927 } 2928 2929 if table.is_empty() { 2930 output.push(String::from("No tokens found")); 2931 } else { 2932 output.push(format!("{table}")); 2933 } 2934 } 2935 2936 /// Auxiliary function to define the token mint subcommand handling. 2937 async fn handle_token_mint(drk: &DrkPtr, parts: &[&str], output: &mut Vec<String>) { 2938 // Check correct command structure 2939 if parts.len() < 5 || parts.len() > 7 { 2940 output.push(String::from("Malformed `token mint` subcommand")); 2941 output.push(String::from( 2942 "Usage: token mint <token> <amount> <recipient> [spend-hook] [user-data]", 2943 )); 2944 return 2945 } 2946 2947 let amount = String::from(parts[3]); 2948 if let Err(e) = f64::from_str(&amount) { 2949 output.push(format!("Invalid amount: {e}")); 2950 return 2951 } 2952 2953 let rcpt = match PublicKey::from_str(parts[4]) { 2954 Ok(r) => r, 2955 Err(e) => { 2956 output.push(format!("Invalid recipient: {e}")); 2957 return 2958 } 2959 }; 2960 2961 let lock = drk.read().await; 2962 let token_id = match lock.get_token(String::from(parts[2])).await { 2963 Ok(t) => t, 2964 Err(e) => { 2965 output.push(format!("Invalid token ID: {e}")); 2966 return 2967 } 2968 }; 2969 2970 // Parse command 2971 let mut index = 5; 2972 let spend_hook = if index < parts.len() { 2973 match FuncId::from_str(parts[index]) { 2974 Ok(s) => Some(s), 2975 Err(e) => { 2976 output.push(format!("Invalid spend hook: {e}")); 2977 return 2978 } 2979 } 2980 } else { 2981 None 2982 }; 2983 index += 1; 2984 2985 let user_data = if index < parts.len() { 2986 let bytes = match bs58::decode(&parts[index]).into_vec() { 2987 Ok(b) => b, 2988 Err(e) => { 2989 output.push(format!("Invalid user data: {e}")); 2990 return 2991 } 2992 }; 2993 2994 let bytes: [u8; 32] = match bytes.try_into() { 2995 Ok(b) => b, 2996 Err(e) => { 2997 output.push(format!("Invalid user data: {e:?}")); 2998 return 2999 } 3000 }; 3001 3002 let elem: pallas::Base = match pallas::Base::from_repr(bytes).into() { 3003 Some(v) => v, 3004 None => { 3005 output.push(String::from("Invalid user data")); 3006 return 3007 } 3008 }; 3009 3010 Some(elem) 3011 } else { 3012 None 3013 }; 3014 3015 match lock.mint_token(&amount, rcpt, token_id, spend_hook, user_data).await { 3016 Ok(t) => output.push(base64::encode(&serialize_async(&t).await)), 3017 Err(e) => output.push(format!("Failed to create token mint transaction: {e}")), 3018 } 3019 } 3020 3021 /// Auxiliary function to define the token freeze subcommand handling. 3022 async fn handle_token_freeze(drk: &DrkPtr, parts: &[&str], output: &mut Vec<String>) { 3023 // Check correct subcommand structure 3024 if parts.len() != 3 { 3025 output.push(String::from("Malformed `token freeze` subcommand")); 3026 output.push(String::from("Usage: token freeze <token>")); 3027 return 3028 } 3029 3030 let lock = drk.read().await; 3031 let token_id = match lock.get_token(String::from(parts[2])).await { 3032 Ok(t) => t, 3033 Err(e) => { 3034 output.push(format!("Invalid token ID: {e}")); 3035 return 3036 } 3037 }; 3038 3039 match lock.freeze_token(token_id).await { 3040 Ok(t) => output.push(base64::encode(&serialize_async(&t).await)), 3041 Err(e) => output.push(format!("Failed to create token freeze transaction: {e}")), 3042 } 3043 } 3044 3045 /// Auxiliary function to define the contract command handling. 3046 async fn handle_contract(drk: &DrkPtr, parts: &[&str], output: &mut Vec<String>) { 3047 // Check correct command structure 3048 if parts.len() < 2 { 3049 output.push(String::from("Malformed `contract` command")); 3050 output.push(String::from("Usage: contract (generate-deploy|list|export-data|deploy|lock)")); 3051 return 3052 } 3053 3054 // Handle subcommand 3055 match parts[1] { 3056 "generate-deploy" => handle_contract_generate_deploy(drk, parts, output).await, 3057 "list" => handle_contract_list(drk, parts, output).await, 3058 "export-data" => handle_contract_export_data(drk, parts, output).await, 3059 "deploy" => handle_contract_deploy(drk, parts, output).await, 3060 "lock" => handle_contract_lock(drk, parts, output).await, 3061 _ => { 3062 output.push(format!("Unrecognized contract subcommand: {}", parts[1])); 3063 output.push(String::from( 3064 "Usage: contract (generate-deploy|list|export-data|deploy|lock)", 3065 )); 3066 } 3067 } 3068 } 3069 3070 /// Auxiliary function to define the contract generate deploy subcommand handling. 3071 async fn handle_contract_generate_deploy(drk: &DrkPtr, parts: &[&str], output: &mut Vec<String>) { 3072 // Check correct subcommand structure 3073 if parts.len() != 2 { 3074 output.push(String::from("Malformed `contract generate-deploy` subcommand")); 3075 output.push(String::from("Usage: contract generate-deploy")); 3076 return 3077 } 3078 3079 if let Err(e) = drk.read().await.deploy_auth_keygen(output).await { 3080 output.push(format!("Error creating deploy auth keypair: {e}")); 3081 } 3082 } 3083 3084 /// Auxiliary function to define the contract list subcommand handling. 3085 async fn handle_contract_list(drk: &DrkPtr, parts: &[&str], output: &mut Vec<String>) { 3086 // Check correct subcommand structure 3087 if parts.len() != 2 && parts.len() != 3 { 3088 output.push(String::from("Malformed `contract list` subcommand")); 3089 output.push(String::from("Usage: contract list [contract-id]")); 3090 return 3091 } 3092 3093 if parts.len() == 3 { 3094 let deploy_auth = match ContractId::from_str(parts[2]) { 3095 Ok(d) => d, 3096 Err(e) => { 3097 output.push(format!("Invalid deploy authority: {e}")); 3098 return 3099 } 3100 }; 3101 3102 let history = match drk.read().await.get_deploy_auth_history(&deploy_auth).await { 3103 Ok(a) => a, 3104 Err(e) => { 3105 output.push(format!("Failed to fetch deploy authority history records: {e}")); 3106 return 3107 } 3108 }; 3109 3110 let mut table = Table::new(); 3111 table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); 3112 table.set_titles(row!["Transaction Hash", "Type", "Block Height"]); 3113 3114 for (tx_hash, tx_type, block_height) in history { 3115 table.add_row(row![tx_hash, tx_type, block_height]); 3116 } 3117 3118 if table.is_empty() { 3119 output.push(String::from("No history records found")); 3120 } else { 3121 output.push(format!("{table}")); 3122 } 3123 return 3124 } 3125 3126 let auths = match drk.read().await.list_deploy_auth().await { 3127 Ok(a) => a, 3128 Err(e) => { 3129 output.push(format!("Failed to fetch deploy authorities: {e}")); 3130 return 3131 } 3132 }; 3133 3134 let mut table = Table::new(); 3135 table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); 3136 table.set_titles(row!["Contract ID", "Secret Key", "Locked", "Lock Height"]); 3137 3138 for (contract_id, secret_key, is_locked, lock_height) in auths { 3139 let lock_height = match lock_height { 3140 Some(lock_height) => lock_height.to_string(), 3141 None => String::from("-"), 3142 }; 3143 table.add_row(row![contract_id, secret_key, is_locked, lock_height]); 3144 } 3145 3146 if table.is_empty() { 3147 output.push(String::from("No deploy authorities found")); 3148 } else { 3149 output.push(format!("{table}")); 3150 } 3151 } 3152 3153 /// Auxiliary function to define the contract export data subcommand handling. 3154 async fn handle_contract_export_data(drk: &DrkPtr, parts: &[&str], output: &mut Vec<String>) { 3155 // Check correct subcommand structure 3156 if parts.len() != 3 { 3157 output.push(String::from("Malformed `contract export-data` subcommand")); 3158 output.push(String::from("Usage: contract export-data <tx-hash>")); 3159 return 3160 } 3161 3162 match drk.read().await.get_deploy_history_record_data(parts[2]).await { 3163 Ok(pair) => output.push(base64::encode(&serialize_async(&pair).await)), 3164 Err(e) => output.push(format!("Failed to retrieve history record: {e}")), 3165 } 3166 } 3167 3168 /// Auxiliary function to define the contract deploy subcommand handling. 3169 async fn handle_contract_deploy(drk: &DrkPtr, parts: &[&str], output: &mut Vec<String>) { 3170 // Check correct subcommand structure 3171 if parts.len() != 4 && parts.len() != 5 { 3172 output.push(String::from("Malformed `contract deploy` subcommand")); 3173 output.push(String::from("Usage: contract deploy <deploy-auth> <wasm-path> [deploy-ix]")); 3174 return 3175 } 3176 3177 let deploy_auth = match ContractId::from_str(parts[2]) { 3178 Ok(d) => d, 3179 Err(e) => { 3180 output.push(format!("Invalid deploy authority: {e}")); 3181 return 3182 } 3183 }; 3184 3185 // Read the wasm bincode and deploy instruction 3186 let file_path = match expand_path(parts[3]) { 3187 Ok(p) => p, 3188 Err(e) => { 3189 output.push(format!("Error while expanding wasm bincode file path: {e}")); 3190 return 3191 } 3192 }; 3193 let wasm_bin = match smol::fs::read(file_path).await { 3194 Ok(w) => w, 3195 Err(e) => { 3196 output.push(format!("Error while reading wasm bincode file: {e}")); 3197 return 3198 } 3199 }; 3200 3201 let deploy_ix = if parts.len() == 5 { 3202 let file_path = match expand_path(parts[4]) { 3203 Ok(p) => p, 3204 Err(e) => { 3205 output.push(format!("Error while expanding deploy instruction file path: {e}")); 3206 return 3207 } 3208 }; 3209 match smol::fs::read(file_path).await { 3210 Ok(d) => d, 3211 Err(e) => { 3212 output.push(format!("Error while reading deploy instruction file: {e}")); 3213 return 3214 } 3215 } 3216 } else { 3217 vec![] 3218 }; 3219 3220 match drk.read().await.deploy_contract(&deploy_auth, wasm_bin, deploy_ix).await { 3221 Ok(t) => output.push(base64::encode(&serialize_async(&t).await)), 3222 Err(e) => output.push(format!("Failed to create contract deployment transaction: {e}")), 3223 } 3224 } 3225 3226 /// Auxiliary function to define the contract lock subcommand handling. 3227 async fn handle_contract_lock(drk: &DrkPtr, parts: &[&str], output: &mut Vec<String>) { 3228 // Check correct subcommand structure 3229 if parts.len() != 3 { 3230 output.push(String::from("Malformed `contract lock` subcommand")); 3231 output.push(String::from("Usage: contract lock <deploy-auth>")); 3232 return 3233 } 3234 3235 let deploy_auth = match ContractId::from_str(parts[2]) { 3236 Ok(d) => d, 3237 Err(e) => { 3238 output.push(format!("Invalid deploy authority: {e}")); 3239 return 3240 } 3241 }; 3242 3243 match drk.read().await.lock_contract(&deploy_auth).await { 3244 Ok(t) => output.push(base64::encode(&serialize_async(&t).await)), 3245 Err(e) => output.push(format!("Failed to create contract lock transaction: {e}")), 3246 } 3247 }