cli_util.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 use std::{ 19 io::{stdin, Cursor, Read}, 20 str::FromStr, 21 }; 22 23 use rodio::{Decoder, OutputStream, Sink}; 24 use smol::channel::Sender; 25 use structopt_toml::clap::{App, Arg, Shell, SubCommand}; 26 27 use darkfi::{ 28 cli_desc, 29 tx::Transaction, 30 util::{encoding::base64, parse::decode_base10}, 31 Error, Result, 32 }; 33 use darkfi_money_contract::model::TokenId; 34 use darkfi_serial::deserialize_async; 35 36 use crate::{money::BALANCE_BASE10_DECIMALS, Drk}; 37 38 /// Auxiliary function to parse a base64 encoded transaction from stdin. 39 pub async fn parse_tx_from_stdin() -> Result<Transaction> { 40 let mut buf = String::new(); 41 stdin().read_to_string(&mut buf)?; 42 match base64::decode(buf.trim()) { 43 Some(bytes) => Ok(deserialize_async(&bytes).await?), 44 None => Err(Error::ParseFailed("Failed to decode transaction")), 45 } 46 } 47 48 /// Auxiliary function to parse a base64 encoded transaction from 49 /// provided input or fallback to stdin if its empty. 50 pub async fn parse_tx_from_input(input: &[String]) -> Result<Transaction> { 51 match input.len() { 52 0 => parse_tx_from_stdin().await, 53 1 => match base64::decode(input[0].trim()) { 54 Some(bytes) => Ok(deserialize_async(&bytes).await?), 55 None => Err(Error::ParseFailed("Failed to decode transaction")), 56 }, 57 _ => Err(Error::ParseFailed("Multiline input provided")), 58 } 59 } 60 61 /// Auxiliary function to parse provided string into a values pair. 62 pub fn parse_value_pair(s: &str) -> Result<(u64, u64)> { 63 let v: Vec<&str> = s.split(':').collect(); 64 if v.len() != 2 { 65 return Err(Error::ParseFailed("Invalid value pair. Use a pair such as 13.37:11.0")) 66 } 67 68 let val0 = decode_base10(v[0], BALANCE_BASE10_DECIMALS, true); 69 let val1 = decode_base10(v[1], BALANCE_BASE10_DECIMALS, true); 70 71 if val0.is_err() || val1.is_err() { 72 return Err(Error::ParseFailed("Invalid value pair. Use a pair such as 13.37:11.0")) 73 } 74 75 Ok((val0.unwrap(), val1.unwrap())) 76 } 77 78 /// Auxiliary function to parse provided string into a tokens pair. 79 pub async fn parse_token_pair(drk: &Drk, s: &str) -> Result<(TokenId, TokenId)> { 80 let v: Vec<&str> = s.split(':').collect(); 81 if v.len() != 2 { 82 return Err(Error::ParseFailed( 83 "Invalid token pair. Use a pair such as:\nWCKD:MLDY\nor\n\ 84 A7f1RKsCUUHrSXA7a9ogmwg8p3bs6F47ggsW826HD4yd:FCuoMii64H5Ee4eVWBjP18WTFS8iLUJmGi16Qti1xFQ2" 85 )) 86 } 87 88 let tok0 = drk.get_token(v[0].to_string()).await; 89 let tok1 = drk.get_token(v[1].to_string()).await; 90 91 if tok0.is_err() || tok1.is_err() { 92 return Err(Error::ParseFailed( 93 "Invalid token pair. Use a pair such as:\nWCKD:MLDY\nor\n\ 94 A7f1RKsCUUHrSXA7a9ogmwg8p3bs6F47ggsW826HD4yd:FCuoMii64H5Ee4eVWBjP18WTFS8iLUJmGi16Qti1xFQ2" 95 )) 96 } 97 98 Ok((tok0.unwrap(), tok1.unwrap())) 99 } 100 101 /// Fun police go away 102 pub async fn kaching() { 103 const WALLET_MP3: &[u8] = include_bytes!("../wallet.mp3"); 104 105 let cursor = Cursor::new(WALLET_MP3); 106 107 let Ok((_stream, stream_handle)) = OutputStream::try_default() else { return }; 108 let Ok(sink) = Sink::try_new(&stream_handle) else { return }; 109 110 let Ok(source) = Decoder::new(cursor) else { return }; 111 sink.append(source); 112 113 sink.sleep_until_end(); 114 } 115 116 /// Auxiliary function to generate provided shell completions. 117 pub fn generate_completions(shell: &str) -> Result<String> { 118 // Sub-commands 119 120 // Interactive 121 let interactive = SubCommand::with_name("interactive").about("Enter Drk interactive shell"); 122 123 // Kaching 124 let kaching = SubCommand::with_name("kaching").about("Fun"); 125 126 // Ping 127 let ping = 128 SubCommand::with_name("ping").about("Send a ping request to the darkfid RPC endpoint"); 129 130 // Completions 131 let shell_arg = Arg::with_name("shell").help("The Shell you want to generate script for"); 132 133 let completions = SubCommand::with_name("completions") 134 .about("Generate a SHELL completion script and print to stdout") 135 .arg(shell_arg); 136 137 // Wallet 138 let initialize = SubCommand::with_name("initialize").about("Initialize wallet database"); 139 140 let keygen = SubCommand::with_name("keygen").about("Generate a new keypair in the wallet"); 141 142 let balance = SubCommand::with_name("balance").about("Query the wallet for known balances"); 143 144 let address = SubCommand::with_name("address").about("Get the default address in the wallet"); 145 146 let addresses = 147 SubCommand::with_name("addresses").about("Print all the addresses in the wallet"); 148 149 let index = Arg::with_name("index").help("Identifier of the address"); 150 151 let default_address = SubCommand::with_name("default-address") 152 .about("Set the default address in the wallet") 153 .arg(index); 154 155 let secrets = 156 SubCommand::with_name("secrets").about("Print all the secret keys from the wallet"); 157 158 let import_secrets = SubCommand::with_name("import-secrets") 159 .about("Import secret keys from stdin into the wallet, separated by newlines"); 160 161 let tree = SubCommand::with_name("tree").about("Print the Merkle tree in the wallet"); 162 163 let coins = SubCommand::with_name("coins").about("Print all the coins in the wallet"); 164 165 let wallet = SubCommand::with_name("wallet").about("Wallet operations").subcommands(vec![ 166 initialize, 167 keygen, 168 balance, 169 address, 170 addresses, 171 default_address, 172 secrets, 173 import_secrets, 174 tree, 175 coins, 176 ]); 177 178 // Spend 179 let spend = SubCommand::with_name("spend") 180 .about("Read a transaction from stdin and mark its input coins as spent"); 181 182 // Unspend 183 let coin = Arg::with_name("coin").help("base64-encoded coin to mark as unspent"); 184 185 let unspend = SubCommand::with_name("unspend").about("Unspend a coin").arg(coin); 186 187 // Transfer 188 let amount = Arg::with_name("amount").help("Amount to send"); 189 190 let token = Arg::with_name("token").help("Token ID to send"); 191 192 let recipient = Arg::with_name("recipient").help("Recipient address"); 193 194 let spend_hook = Arg::with_name("spend-hook").help("Optional contract spend hook to use"); 195 196 let user_data = Arg::with_name("user-data").help("Optional user data to use"); 197 198 let half_split = Arg::with_name("half-split") 199 .long("half-split") 200 .help("Split the output coin into two equal halves"); 201 202 let transfer = 203 SubCommand::with_name("transfer").about("Create a payment transaction").args(&vec![ 204 amount.clone(), 205 token.clone(), 206 recipient.clone(), 207 spend_hook.clone(), 208 user_data.clone(), 209 half_split, 210 ]); 211 212 // Otc 213 let value_pair = Arg::with_name("value-pair") 214 .short("v") 215 .long("value-pair") 216 .takes_value(true) 217 .help("Value pair to send:recv (11.55:99.42)"); 218 219 let token_pair = Arg::with_name("token-pair") 220 .short("t") 221 .long("token-pair") 222 .takes_value(true) 223 .help("Token pair to send:recv (f00:b4r)"); 224 225 let init = SubCommand::with_name("init") 226 .about("Initialize the first half of the atomic swap") 227 .args(&vec![value_pair, token_pair]); 228 229 let join = 230 SubCommand::with_name("join").about("Build entire swap tx given the first half from stdin"); 231 232 let inspect = SubCommand::with_name("inspect") 233 .about("Inspect a swap half or the full swap tx from stdin"); 234 235 let sign = SubCommand::with_name("sign").about("Sign a swap transaction given from stdin"); 236 237 let otc = SubCommand::with_name("otc") 238 .about("OTC atomic swap") 239 .subcommands(vec![init, join, inspect, sign]); 240 241 // DAO 242 let proposer_limit = Arg::with_name("proposer-limit") 243 .help("The minimum amount of governance tokens needed to open a proposal for this DAO"); 244 245 let quorum = Arg::with_name("quorum") 246 .help("Minimal threshold of participating total tokens needed for a proposal to pass"); 247 248 let early_exec_quorum = Arg::with_name("early-exec-quorum") 249 .help("Minimal threshold of participating total tokens needed for a proposal to be considered as strongly supported, enabling early execution. Must be greater or equal to normal quorum."); 250 251 let approval_ratio = Arg::with_name("approval-ratio") 252 .help("The ratio of winning votes/total votes needed for a proposal to pass (2 decimals)"); 253 254 let gov_token_id = Arg::with_name("gov-token-id").help("DAO's governance token ID"); 255 256 let create = SubCommand::with_name("create").about("Create DAO parameters").args(&vec![ 257 proposer_limit, 258 quorum, 259 early_exec_quorum, 260 approval_ratio, 261 gov_token_id, 262 ]); 263 264 let view = SubCommand::with_name("view").about("View DAO data from stdin"); 265 266 let name = Arg::with_name("name").help("Name identifier for the DAO"); 267 268 let import = SubCommand::with_name("import") 269 .about("Import DAO data from stdin") 270 .args(&vec![name.clone()]); 271 272 let opt_name = Arg::with_name("dao-alias").help("Name identifier for the DAO (optional)"); 273 274 let list = SubCommand::with_name("list") 275 .about("List imported DAOs (or info about a specific one)") 276 .args(&vec![opt_name]); 277 278 let balance = SubCommand::with_name("balance") 279 .about("Show the balance of a DAO") 280 .args(&vec![name.clone()]); 281 282 let mint = SubCommand::with_name("mint") 283 .about("Mint an imported DAO on-chain") 284 .args(&vec![name.clone()]); 285 286 let duration = Arg::with_name("duration").help("Duration of the proposal, in block windows"); 287 288 let propose_transfer = SubCommand::with_name("propose-transfer") 289 .about("Create a transfer proposal for a DAO") 290 .args(&vec![ 291 name.clone(), 292 duration.clone(), 293 amount, 294 token, 295 recipient, 296 spend_hook.clone(), 297 user_data.clone(), 298 ]); 299 300 let propose_generic = SubCommand::with_name("propose-generic") 301 .about("Create a generic proposal for a DAO") 302 .args(&vec![name.clone(), duration, user_data.clone()]); 303 304 let proposals = 305 SubCommand::with_name("proposals").about("List DAO proposals").args(&vec![name]); 306 307 let bulla = Arg::with_name("bulla").help("Bulla identifier for the proposal"); 308 309 let export = Arg::with_name("export").help("Encrypt the proposal and encode it to base64"); 310 311 let mint_proposal = Arg::with_name("mint-proposal").help("Create the proposal transaction"); 312 313 let proposal = SubCommand::with_name("proposal").about("View a DAO proposal data").args(&vec![ 314 bulla.clone(), 315 export, 316 mint_proposal, 317 ]); 318 319 let proposal_import = SubCommand::with_name("proposal-import") 320 .about("Import a base64 encoded and encrypted proposal from stdin"); 321 322 let vote = Arg::with_name("vote").help("Vote (0 for NO, 1 for YES)"); 323 324 let vote_weight = 325 Arg::with_name("vote-weight").help("Optional vote weight (amount of governance tokens)"); 326 327 let vote = SubCommand::with_name("vote").about("Vote on a given proposal").args(&vec![ 328 bulla.clone(), 329 vote, 330 vote_weight, 331 ]); 332 333 let early = Arg::with_name("early").long("early").help("Execute the proposal early"); 334 335 let exec = 336 SubCommand::with_name("exec").about("Execute a DAO proposal").args(&vec![bulla, early]); 337 338 let spend_hook_cmd = SubCommand::with_name("spend-hook") 339 .about("Print the DAO contract base64-encoded spend hook"); 340 341 let dao = SubCommand::with_name("dao").about("DAO functionalities").subcommands(vec![ 342 create, 343 view, 344 import, 345 list, 346 balance, 347 mint, 348 propose_transfer, 349 propose_generic, 350 proposals, 351 proposal, 352 proposal_import, 353 vote, 354 exec, 355 spend_hook_cmd, 356 ]); 357 358 // AttachFee 359 let attach_fee = SubCommand::with_name("attach-fee") 360 .about("Attach the fee call to a transaction given from stdin"); 361 362 // Inspect 363 let inspect = SubCommand::with_name("inspect").about("Inspect a transaction from stdin"); 364 365 // Broadcast 366 let broadcast = 367 SubCommand::with_name("broadcast").about("Read a transaction from stdin and broadcast it"); 368 369 // Scan 370 let reset = Arg::with_name("reset") 371 .long("reset") 372 .help("Reset wallet state to provided block height and start scanning"); 373 374 let scan = SubCommand::with_name("scan") 375 .about("Scan the blockchain and parse relevant transactions") 376 .args(&vec![reset]); 377 378 // Explorer 379 let tx_hash = Arg::with_name("tx-hash").help("Transaction hash"); 380 381 let encode = Arg::with_name("encode").long("encode").help("Encode transaction to base64"); 382 383 let fetch_tx = SubCommand::with_name("fetch-tx") 384 .about("Fetch a blockchain transaction by hash") 385 .args(&vec![tx_hash, encode]); 386 387 let simulate_tx = 388 SubCommand::with_name("simulate-tx").about("Read a transaction from stdin and simulate it"); 389 390 let tx_hash = Arg::with_name("tx-hash").help("Fetch specific history record (optional)"); 391 392 let encode = Arg::with_name("encode") 393 .long("encode") 394 .help("Encode specific history record transaction to base64"); 395 396 let txs_history = SubCommand::with_name("txs-history") 397 .about("Fetch broadcasted transactions history") 398 .args(&vec![tx_hash, encode]); 399 400 let clear_reverted = 401 SubCommand::with_name("clear-reverted").about("Remove reverted transactions from history"); 402 403 let height = Arg::with_name("height").help("Fetch specific height record (optional)"); 404 405 let scanned_blocks = SubCommand::with_name("scanned-blocks") 406 .about("Fetch scanned blocks records") 407 .args(&vec![height]); 408 409 let explorer = SubCommand::with_name("explorer") 410 .about("Explorer related subcommands") 411 .subcommands(vec![fetch_tx, simulate_tx, txs_history, clear_reverted, scanned_blocks]); 412 413 // Alias 414 let alias = Arg::with_name("alias").help("Token alias"); 415 416 let token = Arg::with_name("token").help("Token to create alias for"); 417 418 let add = SubCommand::with_name("add").about("Create a Token alias").args(&vec![alias, token]); 419 420 let alias = Arg::with_name("alias") 421 .short("a") 422 .long("alias") 423 .takes_value(true) 424 .help("Token alias to search for"); 425 426 let token = Arg::with_name("token") 427 .short("t") 428 .long("token") 429 .takes_value(true) 430 .help("Token to search alias for"); 431 432 let show = SubCommand::with_name("show") 433 .about( 434 "Print alias info of optional arguments. \ 435 If no argument is provided, list all the aliases in the wallet.", 436 ) 437 .args(&vec![alias, token]); 438 439 let alias = Arg::with_name("alias").help("Token alias to remove"); 440 441 let remove = SubCommand::with_name("remove").about("Remove a Token alias").arg(alias); 442 443 let alias = SubCommand::with_name("alias") 444 .about("Manage Token aliases") 445 .subcommands(vec![add, show, remove]); 446 447 // Token 448 let secret_key = Arg::with_name("secret-key").help("Mint authority secret key"); 449 450 let token_blind = Arg::with_name("token-blind").help("Mint authority token blind"); 451 452 let import = SubCommand::with_name("import") 453 .about("Import a mint authority") 454 .args(&vec![secret_key, token_blind]); 455 456 let generate_mint = 457 SubCommand::with_name("generate-mint").about("Generate a new mint authority"); 458 459 let list = 460 SubCommand::with_name("list").about("List token IDs with available mint authorities"); 461 462 let token = Arg::with_name("token").help("Token ID to mint"); 463 464 let amount = Arg::with_name("amount").help("Amount to mint"); 465 466 let recipient = Arg::with_name("recipient").help("Recipient of the minted tokens"); 467 468 let mint = SubCommand::with_name("mint") 469 .about("Mint tokens") 470 .args(&vec![token, amount, recipient, spend_hook, user_data]); 471 472 let token = Arg::with_name("token").help("Token ID to freeze"); 473 474 let freeze = SubCommand::with_name("freeze").about("Freeze a token mint").arg(token); 475 476 let token = SubCommand::with_name("token").about("Token functionalities").subcommands(vec![ 477 import, 478 generate_mint, 479 list, 480 mint, 481 freeze, 482 ]); 483 484 // Contract 485 let generate_deploy = 486 SubCommand::with_name("generate-deploy").about("Generate a new deploy authority"); 487 488 let contract_id = Arg::with_name("contract-id").help("Contract ID (optional)"); 489 490 let list = SubCommand::with_name("list") 491 .about("List deploy authorities in the wallet (or a specific one)") 492 .args(&vec![contract_id]); 493 494 let tx_hash = Arg::with_name("tx-hash").help("Record transaction hash"); 495 496 let export_data = SubCommand::with_name("export-data") 497 .about("Export a contract history record wasm bincode and deployment instruction, encoded to base64") 498 .args(&vec![tx_hash]); 499 500 let deploy_auth = Arg::with_name("deploy-auth").help("Contract ID (deploy authority)"); 501 502 let wasm_path = Arg::with_name("wasm-path").help("Path to contract wasm bincode"); 503 504 let deploy_ix = 505 Arg::with_name("deploy-ix").help("Optional path to serialized deploy instruction"); 506 507 let deploy = SubCommand::with_name("deploy").about("Deploy a smart contract").args(&vec![ 508 deploy_auth.clone(), 509 wasm_path, 510 deploy_ix, 511 ]); 512 513 let lock = 514 SubCommand::with_name("lock").about("Lock a smart contract").args(&vec![deploy_auth]); 515 516 let contract = SubCommand::with_name("contract") 517 .about("Contract functionalities") 518 .subcommands(vec![generate_deploy, list, export_data, deploy, lock]); 519 520 // Main arguments 521 let config = Arg::with_name("config") 522 .short("c") 523 .long("config") 524 .takes_value(true) 525 .help("Configuration file to use"); 526 527 let network = Arg::with_name("network") 528 .long("network") 529 .takes_value(true) 530 .help("Blockchain network to use"); 531 532 let command = vec![ 533 interactive, 534 kaching, 535 ping, 536 completions, 537 wallet, 538 spend, 539 unspend, 540 transfer, 541 otc, 542 attach_fee, 543 inspect, 544 broadcast, 545 dao, 546 scan, 547 explorer, 548 alias, 549 token, 550 contract, 551 ]; 552 553 let fun = Arg::with_name("fun") 554 .short("f") 555 .long("fun") 556 .help("Flag indicating whether you want some fun in your life"); 557 558 let log = Arg::with_name("log") 559 .short("l") 560 .long("log") 561 .takes_value(true) 562 .help("Set log file to ouput into"); 563 564 let verbose = Arg::with_name("verbose") 565 .short("v") 566 .multiple(true) 567 .help("Increase verbosity (-vvv supported)"); 568 569 let mut app = App::new("drk") 570 .about(cli_desc!()) 571 .args(&vec![config, network, fun, log, verbose]) 572 .subcommands(command); 573 574 let shell = match Shell::from_str(shell) { 575 Ok(s) => s, 576 Err(e) => return Err(Error::Custom(e)), 577 }; 578 579 let mut buf = vec![]; 580 app.gen_completions_to("./drk", shell, &mut buf); 581 582 Ok(String::from_utf8(buf)?) 583 } 584 585 /// Auxiliary function to print provided string buffer. 586 pub fn print_output(buf: &[String]) { 587 for line in buf { 588 println!("{line}"); 589 } 590 } 591 592 /// Auxiliary function to print or insert provided messages to given 593 /// buffer reference. If a channel sender is provided, the messages 594 /// are send to that instead. 595 pub async fn append_or_print( 596 buf: &mut Vec<String>, 597 sender: Option<&Sender<Vec<String>>>, 598 print: &bool, 599 messages: Vec<String>, 600 ) { 601 // Send the messages to the channel, if provided 602 if let Some(sender) = sender { 603 if let Err(e) = sender.send(messages).await { 604 let err_msg = format!("[append_or_print] Sending messages to channel failed: {e}"); 605 if *print { 606 println!("{err_msg}"); 607 } else { 608 buf.push(err_msg); 609 } 610 } 611 return 612 } 613 614 // Print the messages 615 if *print { 616 for msg in messages { 617 println!("{msg}"); 618 } 619 return 620 } 621 622 // Insert the messages in the buffer 623 for msg in messages { 624 buf.push(msg); 625 } 626 }