/ bin / drk / src / cli_util.rs
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  }