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