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 */
18use std::{
19    io::{stdin, Cursor, Read},
20    str::FromStr,
21};
22
23use rodio::{Decoder, OutputStream, Sink};
24use smol::channel::Sender;
25use structopt_toml::clap::{App, Arg, Shell, SubCommand};
26
27use darkfi::{
28    cli_desc,
29    tx::Transaction,
30    util::{encoding::base64, parse::decode_base10},
31    Error, Result,
32};
33use darkfi_money_contract::model::TokenId;
34use darkfi_serial::deserialize_async;
35
36use crate::{money::BALANCE_BASE10_DECIMALS, Drk};
37
38/// Auxiliary function to parse a base64 encoded transaction from stdin.
39pub 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.
50pub 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.
62pub 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.
79pub 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
102pub 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.
117pub 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.
586pub 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.
595pub 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}