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