drk/
cli_util.rs

1/* This file is part of DarkFi (https://dark.fi)
2 *
3 * Copyright (C) 2020-2026 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    collections::{HashMap, HashSet},
21    io::{stdin, Cursor, Read},
22    slice,
23    str::FromStr,
24};
25
26use rodio::{Decoder, OutputStreamBuilder, Sink};
27use smol::channel::Sender;
28use structopt_toml::clap::{App, Arg, Shell, SubCommand};
29
30use darkfi::{
31    cli_desc,
32    tx::{ContractCallLeaf, Transaction, TransactionBuilder},
33    util::{encoding::base64, parse::decode_base10},
34    zk::Proof,
35    Error, Result,
36};
37use darkfi_money_contract::model::TokenId;
38use darkfi_sdk::{
39    crypto::{keypair::Address, pasta_prelude::PrimeField, FuncId, SecretKey},
40    dark_tree::DarkTree,
41    pasta::pallas,
42    ContractCallImport,
43};
44use darkfi_serial::deserialize_async;
45
46use crate::{money::BALANCE_BASE10_DECIMALS, Drk};
47
48/// Auxiliary function to parse a base64 encoded transaction from stdin.
49pub async fn parse_tx_from_stdin() -> Result<Transaction> {
50    let mut buf = String::new();
51    stdin().read_to_string(&mut buf)?;
52    match base64::decode(buf.trim()) {
53        Some(bytes) => Ok(deserialize_async(&bytes).await?),
54        None => Err(Error::ParseFailed("Failed to decode transaction")),
55    }
56}
57
58/// Auxiliary function to parse a base64 encoded transaction from
59/// provided input or fallback to stdin if its empty.
60pub async fn parse_tx_from_input(input: &[String]) -> Result<Transaction> {
61    match input.len() {
62        0 => parse_tx_from_stdin().await,
63        1 => match base64::decode(input[0].trim()) {
64            Some(bytes) => Ok(deserialize_async(&bytes).await?),
65            None => Err(Error::ParseFailed("Failed to decode transaction")),
66        },
67        _ => Err(Error::ParseFailed("Multiline input provided")),
68    }
69}
70
71/// Auxiliary function to parse base64 encoded contract calls from stdin.
72pub async fn parse_calls_from_stdin() -> Result<Vec<ContractCallImport>> {
73    let lines = stdin().lines();
74    let mut calls = vec![];
75    for line in lines {
76        let Some(line) = base64::decode(&line?) else {
77            return Err(Error::ParseFailed("Failed to decode base64"))
78        };
79        calls.push(deserialize_async(&line).await?);
80    }
81    Ok(calls)
82}
83
84/// Auxiliary function to parse base64 encoded contract calls from
85/// provided input or fallback to stdin if its empty.
86pub async fn parse_calls_from_input(input: &[String]) -> Result<Vec<ContractCallImport>> {
87    if input.is_empty() {
88        return parse_calls_from_stdin().await
89    }
90
91    let mut calls = vec![];
92    for line in input {
93        let Some(line) = base64::decode(line) else {
94            return Err(Error::ParseFailed("Failed to decode base64"))
95        };
96        calls.push(deserialize_async(&line).await?);
97    }
98    Ok(calls)
99}
100
101/// Auxiliary function to parse provided string into a values pair.
102pub fn parse_value_pair(s: &str) -> Result<(u64, u64)> {
103    let v: Vec<&str> = s.split(':').collect();
104    if v.len() != 2 {
105        return Err(Error::ParseFailed("Invalid value pair. Use a pair such as 13.37:11.0"))
106    }
107
108    let val0 = decode_base10(v[0], BALANCE_BASE10_DECIMALS, true);
109    let val1 = decode_base10(v[1], BALANCE_BASE10_DECIMALS, true);
110
111    if val0.is_err() || val1.is_err() {
112        return Err(Error::ParseFailed("Invalid value pair. Use a pair such as 13.37:11.0"))
113    }
114
115    Ok((val0.unwrap(), val1.unwrap()))
116}
117
118/// Auxiliary function to parse provided string into a tokens pair.
119pub async fn parse_token_pair(drk: &Drk, s: &str) -> Result<(TokenId, TokenId)> {
120    let v: Vec<&str> = s.split(':').collect();
121    if v.len() != 2 {
122        return Err(Error::ParseFailed(
123            "Invalid token pair. Use a pair such as:\nWCKD:MLDY\nor\n\
124            A7f1RKsCUUHrSXA7a9ogmwg8p3bs6F47ggsW826HD4yd:FCuoMii64H5Ee4eVWBjP18WTFS8iLUJmGi16Qti1xFQ2"
125        ))
126    }
127
128    let tok0 = drk.get_token(v[0].to_string()).await;
129    let tok1 = drk.get_token(v[1].to_string()).await;
130
131    if tok0.is_err() || tok1.is_err() {
132        return Err(Error::ParseFailed(
133            "Invalid token pair. Use a pair such as:\nWCKD:MLDY\nor\n\
134            A7f1RKsCUUHrSXA7a9ogmwg8p3bs6F47ggsW826HD4yd:FCuoMii64H5Ee4eVWBjP18WTFS8iLUJmGi16Qti1xFQ2"
135        ))
136    }
137
138    Ok((tok0.unwrap(), tok1.unwrap()))
139}
140
141/// Fun police go away
142pub async fn kaching() {
143    const WALLET_MP3: &[u8] = include_bytes!("../wallet.mp3");
144
145    let cursor = Cursor::new(WALLET_MP3);
146
147    let Ok(stream_handle) = OutputStreamBuilder::open_default_stream() else { return };
148    let sink = Sink::connect_new(stream_handle.mixer());
149
150    let Ok(source) = Decoder::new(cursor) else { return };
151    sink.append(source);
152    sink.detach();
153}
154
155/// Auxiliary function to generate provided shell completions.
156pub fn generate_completions(shell: &str) -> Result<String> {
157    // Sub-commands
158
159    // Interactive
160    let interactive = SubCommand::with_name("interactive").about("Enter Drk interactive shell");
161
162    // Kaching
163    let kaching = SubCommand::with_name("kaching").about("Fun");
164
165    // Ping
166    let ping =
167        SubCommand::with_name("ping").about("Send a ping request to the darkfid RPC endpoint");
168
169    // Completions
170    let shell_arg = Arg::with_name("shell").help("The Shell you want to generate script for");
171
172    let completions = SubCommand::with_name("completions")
173        .about("Generate a SHELL completion script and print to stdout")
174        .arg(shell_arg);
175
176    // Wallet
177    let initialize = SubCommand::with_name("initialize").about("Initialize wallet database");
178
179    let keygen = SubCommand::with_name("keygen").about("Generate a new keypair in the wallet");
180
181    let balance = SubCommand::with_name("balance").about("Query the wallet for known balances");
182
183    let address = SubCommand::with_name("address").about("Get the default address in the wallet");
184
185    let addresses =
186        SubCommand::with_name("addresses").about("Print all the addresses in the wallet");
187
188    let index = Arg::with_name("index").help("Identifier of the address");
189
190    let default_address = SubCommand::with_name("default-address")
191        .about("Set the default address in the wallet")
192        .arg(index.clone());
193
194    let secrets =
195        SubCommand::with_name("secrets").about("Print all the secret keys from the wallet");
196
197    let import_secrets = SubCommand::with_name("import-secrets")
198        .about("Import secret keys from stdin into the wallet, separated by newlines");
199
200    let tree = SubCommand::with_name("tree").about("Print the Merkle tree in the wallet");
201
202    let coins = SubCommand::with_name("coins").about("Print all the coins in the wallet");
203
204    let spend_hook = Arg::with_name("spend-hook").help("Optional contract spend hook to use");
205
206    let user_data = Arg::with_name("user-data").help("Optional user data to use");
207
208    let mining_config = SubCommand::with_name("mining-config")
209        .about("Print a wallet address mining configuration")
210        .args(&[index, spend_hook.clone(), user_data.clone()]);
211
212    let wallet = SubCommand::with_name("wallet").about("Wallet operations").subcommands(vec![
213        initialize,
214        keygen,
215        balance,
216        address,
217        addresses,
218        default_address,
219        secrets,
220        import_secrets,
221        tree,
222        coins,
223        mining_config,
224    ]);
225
226    // Spend
227    let spend = SubCommand::with_name("spend")
228        .about("Read a transaction from stdin and mark its input coins as spent");
229
230    // Unspend
231    let coin = Arg::with_name("coin").help("base64-encoded coin to mark as unspent");
232
233    let unspend = SubCommand::with_name("unspend").about("Unspend a coin").arg(coin);
234
235    // Transfer
236    let amount = Arg::with_name("amount").help("Amount to send");
237
238    let token = Arg::with_name("token").help("Token ID to send");
239
240    let recipient = Arg::with_name("recipient").help("Recipient address");
241
242    let half_split = Arg::with_name("half-split")
243        .long("half-split")
244        .help("Split the output coin into two equal halves");
245
246    let transfer = SubCommand::with_name("transfer").about("Create a payment transaction").args(&[
247        amount.clone(),
248        token.clone(),
249        recipient.clone(),
250        spend_hook.clone(),
251        user_data.clone(),
252        half_split,
253    ]);
254
255    // Otc
256    let value_pair = Arg::with_name("value-pair")
257        .short("v")
258        .long("value-pair")
259        .takes_value(true)
260        .help("Value pair to send:recv (11.55:99.42)");
261
262    let token_pair = Arg::with_name("token-pair")
263        .short("t")
264        .long("token-pair")
265        .takes_value(true)
266        .help("Token pair to send:recv (f00:b4r)");
267
268    let init = SubCommand::with_name("init")
269        .about("Initialize the first half of the atomic swap")
270        .args(&[value_pair, token_pair]);
271
272    let join =
273        SubCommand::with_name("join").about("Build entire swap tx given the first half from stdin");
274
275    let inspect = SubCommand::with_name("inspect")
276        .about("Inspect a swap half or the full swap tx from stdin");
277
278    let sign = SubCommand::with_name("sign").about("Sign a swap transaction given from stdin");
279
280    let otc = SubCommand::with_name("otc")
281        .about("OTC atomic swap")
282        .subcommands(vec![init, join, inspect, sign]);
283
284    // DAO
285    let proposer_limit = Arg::with_name("proposer-limit")
286        .help("The minimum amount of governance tokens needed to open a proposal for this DAO");
287
288    let quorum = Arg::with_name("quorum")
289        .help("Minimal threshold of participating total tokens needed for a proposal to pass");
290
291    let early_exec_quorum = Arg::with_name("early-exec-quorum")
292        .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.");
293
294    let approval_ratio = Arg::with_name("approval-ratio")
295        .help("The ratio of winning votes/total votes needed for a proposal to pass (2 decimals)");
296
297    let gov_token_id = Arg::with_name("gov-token-id").help("DAO's governance token ID");
298
299    let create = SubCommand::with_name("create").about("Create DAO parameters").args(&[
300        proposer_limit,
301        quorum,
302        early_exec_quorum,
303        approval_ratio,
304        gov_token_id,
305    ]);
306
307    let view = SubCommand::with_name("view").about("View DAO data from stdin");
308
309    let name = Arg::with_name("name").help("Name identifier for the DAO");
310
311    let import = SubCommand::with_name("import")
312        .about("Import DAO data from stdin")
313        .args(slice::from_ref(&name));
314
315    let remove = SubCommand::with_name("remove")
316        .about("Remove a DAO and all its data")
317        .args(slice::from_ref(&name));
318
319    let opt_name = Arg::with_name("dao-alias").help("Name identifier for the DAO (optional)");
320
321    let list = SubCommand::with_name("list")
322        .about("List imported DAOs (or info about a specific one)")
323        .args(&[opt_name]);
324
325    let balance = SubCommand::with_name("balance")
326        .about("Show the balance of a DAO")
327        .args(slice::from_ref(&name));
328
329    let mint = SubCommand::with_name("mint")
330        .about("Mint an imported DAO on-chain")
331        .args(slice::from_ref(&name));
332
333    let duration = Arg::with_name("duration").help("Duration of the proposal, in block windows");
334
335    let propose_transfer = SubCommand::with_name("propose-transfer")
336        .about("Create a transfer proposal for a DAO")
337        .args(&[
338            name.clone(),
339            duration.clone(),
340            amount,
341            token,
342            recipient,
343            spend_hook.clone(),
344            user_data.clone(),
345        ]);
346
347    let propose_generic = SubCommand::with_name("propose-generic")
348        .about("Create a generic proposal for a DAO")
349        .args(&[name.clone(), duration, user_data.clone()]);
350
351    let proposals = SubCommand::with_name("proposals").about("List DAO proposals").arg(&name);
352
353    let bulla = Arg::with_name("bulla").help("Bulla identifier for the proposal");
354
355    let export = Arg::with_name("export").help("Encrypt the proposal and encode it to base64");
356
357    let mint_proposal = Arg::with_name("mint-proposal").help("Create the proposal transaction");
358
359    let proposal = SubCommand::with_name("proposal").about("View a DAO proposal data").args(&[
360        bulla.clone(),
361        export,
362        mint_proposal,
363    ]);
364
365    let proposal_import = SubCommand::with_name("proposal-import")
366        .about("Import a base64 encoded and encrypted proposal from stdin");
367
368    let vote = Arg::with_name("vote").help("Vote (0 for NO, 1 for YES)");
369
370    let vote_weight =
371        Arg::with_name("vote-weight").help("Optional vote weight (amount of governance tokens)");
372
373    let vote = SubCommand::with_name("vote").about("Vote on a given proposal").args(&[
374        bulla.clone(),
375        vote,
376        vote_weight,
377    ]);
378
379    let early = Arg::with_name("early").long("early").help("Execute the proposal early");
380
381    let exec = SubCommand::with_name("exec").about("Execute a DAO proposal").args(&[bulla, early]);
382
383    let spend_hook_cmd = SubCommand::with_name("spend-hook")
384        .about("Print the DAO contract base64-encoded spend hook");
385
386    let mining_config =
387        SubCommand::with_name("mining-config").about("Print a DAO mining configuration").arg(name);
388
389    let dao = SubCommand::with_name("dao").about("DAO functionalities").subcommands(vec![
390        create,
391        view,
392        import,
393        remove,
394        list,
395        balance,
396        mint,
397        propose_transfer,
398        propose_generic,
399        proposals,
400        proposal,
401        proposal_import,
402        vote,
403        exec,
404        spend_hook_cmd,
405        mining_config,
406    ]);
407
408    // AttachFee
409    let attach_fee = SubCommand::with_name("attach-fee")
410        .about("Attach the fee call to a transaction given from stdin");
411
412    // TxFromCalls
413    let calls_map =
414        Arg::with_name("calls-map").help("Optional parent/children dependency map for the calls");
415
416    let tx_from_calls = SubCommand::with_name("tx-from-calls")
417        .about(
418            "Create a transaction from newline-separated calls from stdin and attach the fee call",
419        )
420        .args(&[calls_map]);
421
422    // Inspect
423    let inspect = SubCommand::with_name("inspect").about("Inspect a transaction from stdin");
424
425    // Broadcast
426    let broadcast =
427        SubCommand::with_name("broadcast").about("Read a transaction from stdin and broadcast it");
428
429    // Scan
430    let reset = Arg::with_name("reset")
431        .long("reset")
432        .help("Reset wallet state to provided block height and start scanning");
433
434    let scan = SubCommand::with_name("scan")
435        .about("Scan the blockchain and parse relevant transactions")
436        .args(&[reset]);
437
438    // Explorer
439    let tx_hash = Arg::with_name("tx-hash").help("Transaction hash");
440
441    let encode = Arg::with_name("encode").long("encode").help("Encode transaction to base64");
442
443    let fetch_tx = SubCommand::with_name("fetch-tx")
444        .about("Fetch a blockchain transaction by hash")
445        .args(&[tx_hash, encode]);
446
447    let simulate_tx =
448        SubCommand::with_name("simulate-tx").about("Read a transaction from stdin and simulate it");
449
450    let tx_hash = Arg::with_name("tx-hash").help("Fetch specific history record (optional)");
451
452    let encode = Arg::with_name("encode")
453        .long("encode")
454        .help("Encode specific history record transaction to base64");
455
456    let txs_history = SubCommand::with_name("txs-history")
457        .about("Fetch broadcasted transactions history")
458        .args(&[tx_hash, encode]);
459
460    let clear_reverted =
461        SubCommand::with_name("clear-reverted").about("Remove reverted transactions from history");
462
463    let height = Arg::with_name("height").help("Fetch specific height record (optional)");
464
465    let scanned_blocks = SubCommand::with_name("scanned-blocks")
466        .about("Fetch scanned blocks records")
467        .args(&[height]);
468
469    let mining_config = SubCommand::with_name("mining-config")
470        .about("Read a mining configuration from stdin and display its parts");
471
472    let explorer =
473        SubCommand::with_name("explorer").about("Explorer related subcommands").subcommands(vec![
474            fetch_tx,
475            simulate_tx,
476            txs_history,
477            clear_reverted,
478            scanned_blocks,
479            mining_config,
480        ]);
481
482    // Alias
483    let alias = Arg::with_name("alias").help("Token alias");
484
485    let token = Arg::with_name("token").help("Token to create alias for");
486
487    let add = SubCommand::with_name("add").about("Create a Token alias").args(&[alias, token]);
488
489    let alias = Arg::with_name("alias")
490        .short("a")
491        .long("alias")
492        .takes_value(true)
493        .help("Token alias to search for");
494
495    let token = Arg::with_name("token")
496        .short("t")
497        .long("token")
498        .takes_value(true)
499        .help("Token to search alias for");
500
501    let show = SubCommand::with_name("show")
502        .about(
503            "Print alias info of optional arguments. \
504                    If no argument is provided, list all the aliases in the wallet.",
505        )
506        .args(&[alias, token]);
507
508    let alias = Arg::with_name("alias").help("Token alias to remove");
509
510    let remove = SubCommand::with_name("remove").about("Remove a Token alias").arg(alias);
511
512    let alias = SubCommand::with_name("alias")
513        .about("Manage Token aliases")
514        .subcommands(vec![add, show, remove]);
515
516    // Token
517    let secret_key = Arg::with_name("secret-key").help("Mint authority secret key");
518
519    let token_blind = Arg::with_name("token-blind").help("Mint authority token blind");
520
521    let import = SubCommand::with_name("import")
522        .about("Import a mint authority")
523        .args(&[secret_key, token_blind]);
524
525    let generate_mint =
526        SubCommand::with_name("generate-mint").about("Generate a new mint authority");
527
528    let list =
529        SubCommand::with_name("list").about("List token IDs with available mint authorities");
530
531    let token = Arg::with_name("token").help("Token ID to mint");
532
533    let amount = Arg::with_name("amount").help("Amount to mint");
534
535    let recipient = Arg::with_name("recipient").help("Recipient of the minted tokens");
536
537    let mint = SubCommand::with_name("mint")
538        .about("Mint tokens")
539        .args(&[token, amount, recipient, spend_hook, user_data]);
540
541    let token = Arg::with_name("token").help("Token ID to freeze");
542
543    let freeze = SubCommand::with_name("freeze").about("Freeze a token mint").arg(token);
544
545    let token = SubCommand::with_name("token").about("Token functionalities").subcommands(vec![
546        import,
547        generate_mint,
548        list,
549        mint,
550        freeze,
551    ]);
552
553    // Contract
554    let generate_deploy =
555        SubCommand::with_name("generate-deploy").about("Generate a new deploy authority");
556
557    let contract_id = Arg::with_name("contract-id").help("Contract ID (optional)");
558
559    let list = SubCommand::with_name("list")
560        .about("List deploy authorities in the wallet (or a specific one)")
561        .args(&[contract_id]);
562
563    let tx_hash = Arg::with_name("tx-hash").help("Record transaction hash");
564
565    let export_data = SubCommand::with_name("export-data")
566        .about("Export a contract history record wasm bincode and deployment instruction, encoded to base64")
567        .args(&[tx_hash]);
568
569    let deploy_auth = Arg::with_name("deploy-auth").help("Contract ID (deploy authority)");
570
571    let wasm_path = Arg::with_name("wasm-path").help("Path to contract wasm bincode");
572
573    let deploy_ix =
574        Arg::with_name("deploy-ix").help("Optional path to serialized deploy instruction");
575
576    let deploy = SubCommand::with_name("deploy").about("Deploy a smart contract").args(&[
577        deploy_auth.clone(),
578        wasm_path,
579        deploy_ix,
580    ]);
581
582    let lock = SubCommand::with_name("lock").about("Lock a smart contract").args(&[deploy_auth]);
583
584    let contract = SubCommand::with_name("contract")
585        .about("Contract functionalities")
586        .subcommands(vec![generate_deploy, list, export_data, deploy, lock]);
587
588    // Main arguments
589    let config = Arg::with_name("config")
590        .short("c")
591        .long("config")
592        .takes_value(true)
593        .help("Configuration file to use");
594
595    let network = Arg::with_name("network")
596        .long("network")
597        .takes_value(true)
598        .help("Blockchain network to use");
599
600    let command = vec![
601        interactive,
602        kaching,
603        ping,
604        completions,
605        wallet,
606        spend,
607        unspend,
608        transfer,
609        otc,
610        attach_fee,
611        tx_from_calls,
612        inspect,
613        broadcast,
614        dao,
615        scan,
616        explorer,
617        alias,
618        token,
619        contract,
620    ];
621
622    let fun = Arg::with_name("fun")
623        .short("f")
624        .long("fun")
625        .help("Flag indicating whether you want some fun in your life");
626
627    let log = Arg::with_name("log")
628        .short("l")
629        .long("log")
630        .takes_value(true)
631        .help("Set log file to ouput into");
632
633    let verbose = Arg::with_name("verbose")
634        .short("v")
635        .multiple(true)
636        .help("Increase verbosity (-vvv supported)");
637
638    let mut app = App::new("drk")
639        .about(cli_desc!())
640        .args(&[config, network, fun, log, verbose])
641        .subcommands(command);
642
643    let shell = match Shell::from_str(shell) {
644        Ok(s) => s,
645        Err(e) => return Err(Error::Custom(e)),
646    };
647
648    let mut buf = vec![];
649    app.gen_completions_to("./drk", shell, &mut buf);
650
651    Ok(String::from_utf8(buf)?)
652}
653
654/// Auxiliary function to print provided string buffer.
655pub fn print_output(buf: &[String]) {
656    for line in buf {
657        println!("{line}");
658    }
659}
660
661/// Auxiliary function to print or insert provided messages to given
662/// buffer reference. If a channel sender is provided, the messages
663/// are send to that instead.
664pub async fn append_or_print(
665    buf: &mut Vec<String>,
666    sender: Option<&Sender<Vec<String>>>,
667    print: &bool,
668    messages: Vec<String>,
669) {
670    // Send the messages to the channel, if provided
671    if let Some(sender) = sender {
672        if let Err(e) = sender.send(messages).await {
673            let err_msg = format!("[append_or_print] Sending messages to channel failed: {e}");
674            if *print {
675                println!("{err_msg}");
676            } else {
677                buf.push(err_msg);
678            }
679        }
680        return
681    }
682
683    // Print the messages
684    if *print {
685        for msg in messages {
686            println!("{msg}");
687        }
688        return
689    }
690
691    // Insert the messages in the buffer
692    for msg in messages {
693        buf.push(msg);
694    }
695}
696
697/// Auxiliary function to parse a base64 encoded mining configuration
698/// from stdin.
699pub async fn parse_mining_config_from_stdin(
700) -> Result<(String, String, Option<String>, Option<String>)> {
701    let mut buf = String::new();
702    stdin().read_to_string(&mut buf)?;
703    let config = buf.trim();
704    let (recipient, spend_hook, user_data) = match base64::decode(config) {
705        Some(bytes) => deserialize_async(&bytes).await?,
706        None => return Err(Error::ParseFailed("Failed to decode mining configuration")),
707    };
708    Ok((config.to_string(), recipient, spend_hook, user_data))
709}
710
711/// Auxiliary function to parse a base64 encoded mining configuration
712/// from provided input or fallback to stdin if its empty.
713pub async fn parse_mining_config_from_input(
714    input: &[String],
715) -> Result<(String, String, Option<String>, Option<String>)> {
716    match input.len() {
717        0 => parse_mining_config_from_stdin().await,
718        1 => {
719            let config = input[0].trim();
720            let (recipient, spend_hook, user_data) = match base64::decode(config) {
721                Some(bytes) => deserialize_async(&bytes).await?,
722                None => return Err(Error::ParseFailed("Failed to decode mining configuration")),
723            };
724            Ok((config.to_string(), recipient, spend_hook, user_data))
725        }
726        _ => Err(Error::ParseFailed("Multiline input provided")),
727    }
728}
729
730/// Auxiliary function to display the parts of a mining configuration.
731pub fn display_mining_config(
732    config: &str,
733    recipient_str: &str,
734    spend_hook: &Option<String>,
735    user_data: &Option<String>,
736    output: &mut Vec<String>,
737) {
738    output.push(format!("DarkFi mining configuration address: {config}"));
739
740    match Address::from_str(recipient_str) {
741        Ok(recipient) => {
742            output.push(format!("Recipient: {recipient_str}"));
743            output.push(format!("Public key: {}", recipient.public_key()));
744            output.push(format!("Network: {:?}", recipient.network()));
745        }
746        Err(e) => output.push(format!("Recipient: Invalid ({e})")),
747    }
748
749    let spend_hook = match spend_hook {
750        Some(spend_hook_str) => match FuncId::from_str(spend_hook_str) {
751            Ok(_) => String::from(spend_hook_str),
752            Err(e) => format!("Invalid ({e})"),
753        },
754        None => String::from("-"),
755    };
756    output.push(format!("Spend hook: {spend_hook}"));
757
758    let user_data = match user_data {
759        Some(user_data_str) => match bs58::decode(&user_data_str).into_vec() {
760            Ok(bytes) => match bytes.try_into() {
761                Ok(bytes) => {
762                    if pallas::Base::from_repr(bytes).is_some().into() {
763                        String::from(user_data_str)
764                    } else {
765                        String::from("Invalid")
766                    }
767                }
768                Err(e) => format!("Invalid ({e:?})"),
769            },
770            Err(e) => format!("Invalid ({e})"),
771        },
772        None => String::from("-"),
773    };
774    output.push(format!("User data: {user_data}"));
775}
776
777/// Cast `ContractCallImport` to `ContractCallLeaf`
778fn to_leaf(call: &ContractCallImport) -> ContractCallLeaf {
779    ContractCallLeaf {
780        call: call.call().clone(),
781        proofs: call.proofs().iter().map(|p| Proof::new(p.clone())).collect(),
782    }
783}
784
785/// Recursively build subtree for a DarkTree
786fn build_subtree(
787    idx: usize,
788    calls: &[ContractCallImport],
789    children_map: &HashMap<usize, &Vec<usize>>,
790) -> DarkTree<ContractCallLeaf> {
791    let children_idx = children_map.get(&idx).map(|v| v.as_slice()).unwrap_or(&[]);
792
793    let children: Vec<DarkTree<ContractCallLeaf>> =
794        children_idx.iter().map(|&i| build_subtree(i, calls, children_map)).collect();
795
796    DarkTree::new(to_leaf(&calls[idx]), children, None, None)
797}
798
799/// Build a `Transaction` given a slice of calls and their mapping
800pub fn tx_from_calls_mapped(
801    calls: &[ContractCallImport],
802    map: &[(usize, Vec<usize>)],
803) -> Result<(TransactionBuilder, Vec<SecretKey>)> {
804    assert_eq!(calls.len(), map.len());
805
806    let signature_secrets: Vec<SecretKey> =
807        calls.iter().flat_map(|c| c.secrets().to_vec()).collect();
808
809    let children_map: HashMap<usize, &Vec<usize>> = map.iter().map(|(k, v)| (*k, v)).collect();
810
811    let (root_idx, root_children_idx) = &map[0];
812
813    let root_children: Vec<DarkTree<ContractCallLeaf>> =
814        root_children_idx.iter().map(|&i| build_subtree(i, calls, &children_map)).collect();
815
816    let tx_builder = TransactionBuilder::new(to_leaf(&calls[*root_idx]), root_children)?;
817
818    Ok((tx_builder, signature_secrets))
819}
820
821/// Auxiliary function to parse a contract call mapping.
822///
823/// The mapping is in the format of `{0: [1,2], 1: [], 2:[3], 3:[]}`.
824/// It supports nesting and this kind of logic as expected.
825///
826/// Errors out if there are non-unique keys or cyclic references.
827pub fn parse_tree(input: &str) -> std::result::Result<Vec<(usize, Vec<usize>)>, String> {
828    let s = input
829        .trim()
830        .strip_prefix('{')
831        .and_then(|s| s.strip_suffix('}'))
832        .ok_or("expected {}")?
833        .trim();
834
835    let mut entries = vec![];
836    let mut seen_keys = HashSet::new();
837
838    if s.is_empty() {
839        return Ok(entries)
840    }
841
842    let mut rest = s;
843    while !rest.is_empty() {
844        // Parse key
845        let (key_str, after_key) = rest.split_once(':').ok_or("expected ':'")?;
846        let key: usize = key_str.trim().parse().map_err(|_| "invalid key")?;
847
848        if !seen_keys.insert(key) {
849            return Err(format!("duplicate key: {}", key));
850        }
851
852        // Parse array
853        let after_key = after_key.trim();
854        let arr_start = after_key.strip_prefix('[').ok_or("expected '['")?;
855        let (arr_content, after_arr) = arr_start.split_once(']').ok_or("expected ']'")?;
856
857        let children: Vec<usize> = arr_content
858            .split(',')
859            .map(|s| s.trim())
860            .filter(|s| !s.is_empty())
861            .map(|s| s.parse().map_err(|_| "invalid child"))
862            .collect::<std::result::Result<_, _>>()?;
863
864        entries.push((key, children));
865
866        // Move to next entry
867        rest = after_arr.trim().strip_prefix(',').unwrap_or(after_arr).trim();
868    }
869
870    check_cycles(&entries)?;
871
872    Ok(entries)
873}
874
875fn check_cycles(entries: &[(usize, Vec<usize>)]) -> std::result::Result<(), String> {
876    let graph: HashMap<usize, &Vec<usize>> = entries.iter().map(|(k, v)| (*k, v)).collect();
877    let mut visited = HashSet::new();
878    let mut path = Vec::new();
879
880    fn dfs(
881        node: usize,
882        graph: &HashMap<usize, &Vec<usize>>,
883        visited: &mut HashSet<usize>,
884        path: &mut Vec<usize>,
885    ) -> std::result::Result<(), String> {
886        if let Some(pos) = path.iter().position(|&n| n == node) {
887            let cycle: Vec<_> = path[pos..].iter().chain(&[node]).map(|n| n.to_string()).collect();
888            return Err(format!("cycle detected: {}", cycle.join(" -> ")));
889        }
890
891        if visited.contains(&node) {
892            return Ok(());
893        }
894
895        path.push(node);
896        if let Some(children) = graph.get(&node) {
897            for &child in *children {
898                dfs(child, graph, visited, path)?;
899            }
900        }
901        path.pop();
902        visited.insert(node);
903
904        Ok(())
905    }
906
907    for &(key, _) in entries {
908        dfs(key, &graph, &mut visited, &mut path)?;
909    }
910
911    Ok(())
912}
913
914#[cfg(test)]
915mod tests {
916    use super::*;
917
918    #[test]
919    fn test_parse_tree() {
920        // Valid inputs
921        assert_eq!(parse_tree("{}").unwrap(), vec![]);
922        assert_eq!(parse_tree("{  }").unwrap(), vec![]);
923        assert_eq!(parse_tree("{ 0: [] }").unwrap(), vec![(0, vec![])]);
924        assert_eq!(parse_tree("{ 0: [1, 2, 3] }").unwrap(), vec![(0, vec![1, 2, 3])]);
925        assert_eq!(parse_tree("{0:[],1:[2]}").unwrap(), vec![(0, vec![]), (1, vec![2])]);
926        assert_eq!(parse_tree("{ 0: [], 1: [], }").unwrap(), vec![(0, vec![]), (1, vec![])]);
927        assert_eq!(parse_tree("{ 0: [1, 2,] }").unwrap(), vec![(0, vec![1, 2])]);
928
929        assert_eq!(
930            parse_tree("{ 0: [], 1: [2, 3], 2: [], 3: [4], 4: [] }").unwrap(),
931            vec![(0, vec![]), (1, vec![2, 3]), (2, vec![]), (3, vec![4]), (4, vec![])]
932        );
933
934        assert_eq!(
935            parse_tree("{   0  :  [  ]  ,   1  :  [  2  ,  3  ]   }").unwrap(),
936            vec![(0, vec![]), (1, vec![2, 3])]
937        );
938
939        assert_eq!(
940            parse_tree("{ 999: [1000, 1001], 1000: [], 1001: [] }").unwrap(),
941            vec![(999, vec![1000, 1001]), (1000, vec![]), (1001, vec![])]
942        );
943
944        // Order preservation
945        let keys: Vec<usize> =
946            parse_tree("{ 5: [], 2: [], 9: [], 0: [] }").unwrap().iter().map(|(k, _)| *k).collect();
947        assert_eq!(keys, vec![5, 2, 9, 0]);
948
949        // Valid DAG (not a cycle)
950        assert!(parse_tree("{ 0: [1, 2], 1: [3], 2: [3], 3: [] }").is_ok());
951
952        // Syntax errors
953        assert!(parse_tree("0: [] }").is_err());
954        assert!(parse_tree("{ 0: []").is_err());
955        assert!(parse_tree("{ 0 [] }").is_err());
956        assert!(parse_tree("{ 0: ] }").is_err());
957        assert!(parse_tree("{ 0: [1, 2 }").is_err());
958        assert!(parse_tree("{ abc: [] }").is_err());
959        assert!(parse_tree("{ 0: [abc] }").is_err());
960        assert!(parse_tree("{ -1: [] }").is_err());
961
962        // Duplicate keys
963        assert!(parse_tree("{ 0: [], 0: [1] }").unwrap_err().contains("duplicate key: 0"));
964        assert!(parse_tree("{ 0: [], 1: [], 2: [], 1: [] }")
965            .unwrap_err()
966            .contains("duplicate key: 1"));
967
968        // Cycle detection
969        let err = parse_tree("{ 0: [0] }").unwrap_err();
970        assert!(err.contains("cycle detected") && err.contains("0 -> 0"));
971
972        let err = parse_tree("{ 0: [1], 1: [0] }").unwrap_err();
973        assert!(err.contains("cycle detected"));
974
975        let err = parse_tree("{ 0: [1], 1: [2], 2: [3], 3: [0] }").unwrap_err();
976        assert!(err.contains("cycle detected") && err.contains("0 -> 1 -> 2 -> 3 -> 0"));
977
978        let err = parse_tree("{ 0: [1], 1: [2], 2: [3], 3: [2] }").unwrap_err();
979        assert!(err.contains("cycle detected") && err.contains("2 -> 3 -> 2"));
980    }
981}