1use 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, fs::read_to_string};
28use structopt_toml::{
29 clap::{App, Arg, Shell, SubCommand},
30 structopt::StructOpt,
31 StructOptToml,
32};
33use url::Url;
34
35use darkfi::{
36 cli_desc,
37 tx::{ContractCallLeaf, Transaction, TransactionBuilder},
38 util::{encoding::base64, parse::decode_base10, path::get_config_path},
39 zk::Proof,
40 Error, Result,
41};
42use darkfi_money_contract::model::TokenId;
43use darkfi_sdk::{
44 crypto::{
45 keypair::{Address, Network},
46 pasta_prelude::PrimeField,
47 FuncId, SecretKey,
48 },
49 dark_tree::DarkTree,
50 pasta::pallas,
51 ContractCallImport,
52};
53use darkfi_serial::deserialize_async;
54
55use crate::{money::BALANCE_BASE10_DECIMALS, Drk};
56
57#[derive(Clone, Debug, serde::Deserialize, structopt::StructOpt, structopt_toml::StructOptToml)]
60#[structopt()]
61pub struct BlockchainNetwork {
62 #[structopt(long, default_value = "~/.local/share/darkfi/drk/localnet/cache")]
63 pub cache_path: String,
65
66 #[structopt(long, default_value = "~/.local/share/darkfi/drk/localnet/wallet.db")]
67 pub wallet_path: String,
69
70 #[structopt(long, default_value = "changeme")]
71 pub wallet_pass: String,
73
74 #[structopt(short, long, default_value = "tcp://127.0.0.1:28345")]
75 pub endpoint: Url,
77
78 #[structopt(long, default_value = "~/.local/share/darkfi/drk/localnet/history.txt")]
79 pub history_path: String,
81}
82
83pub async fn parse_blockchain_config(
86 config: Option<String>,
87 network: &str,
88 fallback: &str,
89) -> Result<(Network, BlockchainNetwork)> {
90 let used_net = match network {
92 "mainnet" | "localnet" => Network::Mainnet,
93 "testnet" => Network::Testnet,
94 _ => return Err(Error::ParseFailed("Invalid blockchain network")),
95 };
96
97 let config_path = get_config_path(config, fallback)?;
99
100 let contents = read_to_string(&config_path).await?;
102 let contents: toml::Value = match toml::from_str(&contents) {
103 Ok(v) => v,
104 Err(e) => {
105 eprintln!("Failed parsing TOML config: {e}");
106 return Err(Error::ParseFailed("Failed parsing TOML config"))
107 }
108 };
109
110 let Some(table) = contents.as_table() else { return Err(Error::ParseFailed("TOML not a map")) };
112 let Some(network_configs) = table.get("network_config") else {
113 return Err(Error::ParseFailed("TOML does not contain network configurations"))
114 };
115 let Some(network_configs) = network_configs.as_table() else {
116 return Err(Error::ParseFailed("`network_config` not a map"))
117 };
118 let Some(network_config) = network_configs.get(network) else {
119 return Err(Error::ParseFailed("TOML does not contain requested network configuration"))
120 };
121 let network_config = toml::to_string(&network_config).unwrap();
122 let network_config =
123 match BlockchainNetwork::from_iter_with_toml::<Vec<String>>(&network_config, vec![]) {
124 Ok(v) => v,
125 Err(e) => {
126 eprintln!("Failed parsing requested network configuration: {e}");
127 return Err(Error::ParseFailed("Failed parsing requested network configuration"))
128 }
129 };
130
131 Ok((used_net, network_config))
132}
133
134pub async fn parse_tx_from_stdin() -> Result<Transaction> {
136 let mut buf = String::new();
137 stdin().read_to_string(&mut buf)?;
138 match base64::decode(buf.trim()) {
139 Some(bytes) => Ok(deserialize_async(&bytes).await?),
140 None => Err(Error::ParseFailed("Failed to decode transaction")),
141 }
142}
143
144pub async fn parse_tx_from_input(input: &[String]) -> Result<Transaction> {
147 match input.len() {
148 0 => parse_tx_from_stdin().await,
149 1 => match base64::decode(input[0].trim()) {
150 Some(bytes) => Ok(deserialize_async(&bytes).await?),
151 None => Err(Error::ParseFailed("Failed to decode transaction")),
152 },
153 _ => Err(Error::ParseFailed("Multiline input provided")),
154 }
155}
156
157pub async fn parse_calls_from_stdin() -> Result<Vec<ContractCallImport>> {
159 let lines = stdin().lines();
160 let mut calls = vec![];
161 for line in lines {
162 let Some(line) = base64::decode(&line?) else {
163 return Err(Error::ParseFailed("Failed to decode base64"))
164 };
165 calls.push(deserialize_async(&line).await?);
166 }
167 Ok(calls)
168}
169
170pub async fn parse_calls_from_input(input: &[String]) -> Result<Vec<ContractCallImport>> {
173 if input.is_empty() {
174 return parse_calls_from_stdin().await
175 }
176
177 let mut calls = vec![];
178 for line in input {
179 let Some(line) = base64::decode(line) else {
180 return Err(Error::ParseFailed("Failed to decode base64"))
181 };
182 calls.push(deserialize_async(&line).await?);
183 }
184 Ok(calls)
185}
186
187pub fn parse_value_pair(s: &str) -> Result<(u64, u64)> {
189 let v: Vec<&str> = s.split(':').collect();
190 if v.len() != 2 {
191 return Err(Error::ParseFailed("Invalid value pair. Use a pair such as 13.37:11.0"))
192 }
193
194 let val0 = decode_base10(v[0], BALANCE_BASE10_DECIMALS, true);
195 let val1 = decode_base10(v[1], BALANCE_BASE10_DECIMALS, true);
196
197 if val0.is_err() || val1.is_err() {
198 return Err(Error::ParseFailed("Invalid value pair. Use a pair such as 13.37:11.0"))
199 }
200
201 Ok((val0.unwrap(), val1.unwrap()))
202}
203
204pub async fn parse_token_pair(drk: &Drk, s: &str) -> Result<(TokenId, TokenId)> {
206 let v: Vec<&str> = s.split(':').collect();
207 if v.len() != 2 {
208 return Err(Error::ParseFailed(
209 "Invalid token pair. Use a pair such as:\nWCKD:MLDY\nor\n\
210 A7f1RKsCUUHrSXA7a9ogmwg8p3bs6F47ggsW826HD4yd:FCuoMii64H5Ee4eVWBjP18WTFS8iLUJmGi16Qti1xFQ2"
211 ))
212 }
213
214 let tok0 = drk.get_token(v[0].to_string()).await;
215 let tok1 = drk.get_token(v[1].to_string()).await;
216
217 if tok0.is_err() || tok1.is_err() {
218 return Err(Error::ParseFailed(
219 "Invalid token pair. Use a pair such as:\nWCKD:MLDY\nor\n\
220 A7f1RKsCUUHrSXA7a9ogmwg8p3bs6F47ggsW826HD4yd:FCuoMii64H5Ee4eVWBjP18WTFS8iLUJmGi16Qti1xFQ2"
221 ))
222 }
223
224 Ok((tok0.unwrap(), tok1.unwrap()))
225}
226
227pub async fn kaching() {
229 const WALLET_MP3: &[u8] = include_bytes!("../wallet.mp3");
230
231 let cursor = Cursor::new(WALLET_MP3);
232
233 let Ok(stream_handle) = OutputStreamBuilder::open_default_stream() else { return };
234 let sink = Sink::connect_new(stream_handle.mixer());
235
236 let Ok(source) = Decoder::new(cursor) else { return };
237 sink.append(source);
238 sink.detach();
239}
240
241pub fn generate_completions(shell: &str) -> Result<String> {
243 let interactive = SubCommand::with_name("interactive").about("Enter Drk interactive shell");
247
248 let kaching = SubCommand::with_name("kaching").about("Fun");
250
251 let ping =
253 SubCommand::with_name("ping").about("Send a ping request to the darkfid RPC endpoint");
254
255 let shell_arg = Arg::with_name("shell").help("The Shell you want to generate script for");
257
258 let completions = SubCommand::with_name("completions")
259 .about("Generate a SHELL completion script and print to stdout")
260 .arg(shell_arg);
261
262 let initialize = SubCommand::with_name("initialize").about("Initialize wallet database");
264
265 let keygen = SubCommand::with_name("keygen").about("Generate a new keypair in the wallet");
266
267 let balance = SubCommand::with_name("balance").about("Query the wallet for known balances");
268
269 let address = SubCommand::with_name("address").about("Get the default address in the wallet");
270
271 let addresses =
272 SubCommand::with_name("addresses").about("Print all the addresses in the wallet");
273
274 let index = Arg::with_name("index").help("Identifier of the address");
275
276 let default_address = SubCommand::with_name("default-address")
277 .about("Set the default address in the wallet")
278 .arg(index.clone());
279
280 let secrets =
281 SubCommand::with_name("secrets").about("Print all the secret keys from the wallet");
282
283 let import_secrets = SubCommand::with_name("import-secrets")
284 .about("Import secret keys from stdin into the wallet, separated by newlines");
285
286 let tree = SubCommand::with_name("tree").about("Print the Merkle tree in the wallet");
287
288 let coins = SubCommand::with_name("coins").about("Print all the coins in the wallet");
289
290 let spend_hook = Arg::with_name("spend-hook").help("Optional contract spend hook to use");
291
292 let user_data = Arg::with_name("user-data").help("Optional user data to use");
293
294 let mining_config = SubCommand::with_name("mining-config")
295 .about("Print a wallet address mining configuration")
296 .args(&[index, spend_hook.clone(), user_data.clone()]);
297
298 let wallet = SubCommand::with_name("wallet").about("Wallet operations").subcommands(vec![
299 initialize,
300 keygen,
301 balance,
302 address,
303 addresses,
304 default_address,
305 secrets,
306 import_secrets,
307 tree,
308 coins,
309 mining_config,
310 ]);
311
312 let spend = SubCommand::with_name("spend")
314 .about("Read a transaction from stdin and mark its input coins as spent");
315
316 let coin = Arg::with_name("coin").help("base64-encoded coin to mark as unspent");
318
319 let unspend = SubCommand::with_name("unspend").about("Unspend a coin").arg(coin);
320
321 let amount = Arg::with_name("amount").help("Amount to send");
323
324 let token = Arg::with_name("token").help("Token ID to send");
325
326 let recipient = Arg::with_name("recipient").help("Recipient address");
327
328 let half_split = Arg::with_name("half-split")
329 .long("half-split")
330 .help("Split the output coin into two equal halves");
331
332 let transfer = SubCommand::with_name("transfer").about("Create a payment transaction").args(&[
333 amount.clone(),
334 token.clone(),
335 recipient.clone(),
336 spend_hook.clone(),
337 user_data.clone(),
338 half_split,
339 ]);
340
341 let value_pair = Arg::with_name("value-pair")
343 .short("v")
344 .long("value-pair")
345 .takes_value(true)
346 .help("Value pair to send:recv (11.55:99.42)");
347
348 let token_pair = Arg::with_name("token-pair")
349 .short("t")
350 .long("token-pair")
351 .takes_value(true)
352 .help("Token pair to send:recv (f00:b4r)");
353
354 let init = SubCommand::with_name("init")
355 .about("Initialize the first half of the atomic swap")
356 .args(&[value_pair, token_pair]);
357
358 let join =
359 SubCommand::with_name("join").about("Build entire swap tx given the first half from stdin");
360
361 let inspect = SubCommand::with_name("inspect")
362 .about("Inspect a swap half or the full swap tx from stdin");
363
364 let sign = SubCommand::with_name("sign").about("Sign a swap transaction given from stdin");
365
366 let otc = SubCommand::with_name("otc")
367 .about("OTC atomic swap")
368 .subcommands(vec![init, join, inspect, sign]);
369
370 let proposer_limit = Arg::with_name("proposer-limit")
372 .help("The minimum amount of governance tokens needed to open a proposal for this DAO");
373
374 let quorum = Arg::with_name("quorum")
375 .help("Minimal threshold of participating total tokens needed for a proposal to pass");
376
377 let early_exec_quorum = Arg::with_name("early-exec-quorum")
378 .help("Minimal threshold of participating total tokens needed for a proposal to be considered strongly supported, enabling early execution. Must be greater than or equal to normal quorum.");
379
380 let approval_ratio = Arg::with_name("approval-ratio")
381 .help("The ratio of yes votes/total votes needed for a proposal to pass (2 decimals)");
382
383 let gov_token_id = Arg::with_name("gov-token-id").help("DAO's governance token ID");
384
385 let create = SubCommand::with_name("create").about("Create DAO parameters").args(&[
386 proposer_limit,
387 quorum,
388 early_exec_quorum,
389 approval_ratio,
390 gov_token_id,
391 ]);
392
393 let view = SubCommand::with_name("view").about("View DAO data from stdin");
394
395 let name = Arg::with_name("name").help("Name identifier for the DAO");
396
397 let import = SubCommand::with_name("import")
398 .about("Import DAO data from stdin")
399 .args(slice::from_ref(&name));
400
401 let remove = SubCommand::with_name("remove")
402 .about("Remove a DAO and all its data")
403 .args(slice::from_ref(&name));
404
405 let opt_name = Arg::with_name("dao-alias").help("Name identifier for the DAO (optional)");
406
407 let list = SubCommand::with_name("list")
408 .about("List imported DAOs (or info about a specific one)")
409 .args(&[opt_name]);
410
411 let balance = SubCommand::with_name("balance")
412 .about("Show the balance of a DAO")
413 .args(slice::from_ref(&name));
414
415 let mint = SubCommand::with_name("mint")
416 .about("Mint an imported DAO on-chain")
417 .args(slice::from_ref(&name));
418
419 let duration = Arg::with_name("duration").help("Duration of the proposal, in block windows");
420
421 let propose_transfer = SubCommand::with_name("propose-transfer")
422 .about("Create a transfer proposal for a DAO")
423 .args(&[
424 name.clone(),
425 duration.clone(),
426 amount,
427 token,
428 recipient,
429 spend_hook.clone(),
430 user_data.clone(),
431 ]);
432
433 let propose_generic = SubCommand::with_name("propose-generic")
434 .about("Create a generic proposal for a DAO")
435 .args(&[name.clone(), duration, user_data.clone()]);
436
437 let proposals = SubCommand::with_name("proposals").about("List DAO proposals").arg(&name);
438
439 let bulla = Arg::with_name("bulla").help("Bulla identifier for the proposal");
440
441 let export = Arg::with_name("export").help("Encrypt the proposal and encode it to base64");
442
443 let mint_proposal = Arg::with_name("mint-proposal").help("Create the proposal transaction");
444
445 let proposal = SubCommand::with_name("proposal").about("View a DAO proposal data").args(&[
446 bulla.clone(),
447 export,
448 mint_proposal,
449 ]);
450
451 let proposal_import = SubCommand::with_name("proposal-import")
452 .about("Import a base64 encoded and encrypted proposal from stdin");
453
454 let vote = Arg::with_name("vote").help("Vote (0 for NO, 1 for YES)");
455
456 let vote_weight =
457 Arg::with_name("vote-weight").help("Optional vote weight (amount of governance tokens)");
458
459 let vote = SubCommand::with_name("vote").about("Vote on a given proposal").args(&[
460 bulla.clone(),
461 vote,
462 vote_weight,
463 ]);
464
465 let early = Arg::with_name("early").long("early").help("Execute the proposal early");
466
467 let exec = SubCommand::with_name("exec").about("Execute a DAO proposal").args(&[bulla, early]);
468
469 let spend_hook_cmd = SubCommand::with_name("spend-hook")
470 .about("Print the DAO contract base64-encoded spend hook");
471
472 let mining_config =
473 SubCommand::with_name("mining-config").about("Print a DAO mining configuration").arg(name);
474
475 let dao = SubCommand::with_name("dao").about("DAO functionalities").subcommands(vec![
476 create,
477 view,
478 import,
479 remove,
480 list,
481 balance,
482 mint,
483 propose_transfer,
484 propose_generic,
485 proposals,
486 proposal,
487 proposal_import,
488 vote,
489 exec,
490 spend_hook_cmd,
491 mining_config,
492 ]);
493
494 let attach_fee = SubCommand::with_name("attach-fee")
496 .about("Attach the fee call to a transaction given from stdin");
497
498 let calls_map =
500 Arg::with_name("calls-map").help("Optional parent/children dependency map for the calls");
501
502 let tx_from_calls = SubCommand::with_name("tx-from-calls")
503 .about(
504 "Create a transaction from newline-separated calls from stdin and attach the fee call",
505 )
506 .args(&[calls_map]);
507
508 let inspect = SubCommand::with_name("inspect").about("Inspect a transaction from stdin");
510
511 let broadcast =
513 SubCommand::with_name("broadcast").about("Read a transaction from stdin and broadcast it");
514
515 let reset = Arg::with_name("reset")
517 .long("reset")
518 .help("Reset wallet state to provided block height and start scanning");
519
520 let scan = SubCommand::with_name("scan")
521 .about("Scan the blockchain and parse relevant transactions")
522 .args(&[reset]);
523
524 let tx_hash = Arg::with_name("tx-hash").help("Transaction hash");
526
527 let encode = Arg::with_name("encode").long("encode").help("Encode transaction to base64");
528
529 let fetch_tx = SubCommand::with_name("fetch-tx")
530 .about("Fetch a blockchain transaction by hash")
531 .args(&[tx_hash, encode]);
532
533 let simulate_tx =
534 SubCommand::with_name("simulate-tx").about("Read a transaction from stdin and simulate it");
535
536 let tx_hash = Arg::with_name("tx-hash").help("Fetch specific history record (optional)");
537
538 let encode = Arg::with_name("encode")
539 .long("encode")
540 .help("Encode specific history record transaction to base64");
541
542 let txs_history = SubCommand::with_name("txs-history")
543 .about("Fetch broadcasted transactions history")
544 .args(&[tx_hash, encode]);
545
546 let clear_reverted =
547 SubCommand::with_name("clear-reverted").about("Remove reverted transactions from history");
548
549 let height = Arg::with_name("height").help("Fetch specific height record (optional)");
550
551 let scanned_blocks = SubCommand::with_name("scanned-blocks")
552 .about("Fetch scanned blocks records")
553 .args(&[height]);
554
555 let mining_config = SubCommand::with_name("mining-config")
556 .about("Read a mining configuration from stdin and display its parts");
557
558 let explorer =
559 SubCommand::with_name("explorer").about("Explorer related subcommands").subcommands(vec![
560 fetch_tx,
561 simulate_tx,
562 txs_history,
563 clear_reverted,
564 scanned_blocks,
565 mining_config,
566 ]);
567
568 let alias = Arg::with_name("alias").help("Token alias");
570
571 let token = Arg::with_name("token").help("Token to create alias for");
572
573 let add = SubCommand::with_name("add").about("Create a Token alias").args(&[alias, token]);
574
575 let alias = Arg::with_name("alias")
576 .short("a")
577 .long("alias")
578 .takes_value(true)
579 .help("Token alias to search for");
580
581 let token = Arg::with_name("token")
582 .short("t")
583 .long("token")
584 .takes_value(true)
585 .help("Token to search alias for");
586
587 let show = SubCommand::with_name("show")
588 .about(
589 "Print alias info of optional arguments. \
590 If no argument is provided, list all the aliases in the wallet.",
591 )
592 .args(&[alias, token]);
593
594 let alias = Arg::with_name("alias").help("Token alias to remove");
595
596 let remove = SubCommand::with_name("remove").about("Remove a Token alias").arg(alias);
597
598 let alias = SubCommand::with_name("alias")
599 .about("Manage Token aliases")
600 .subcommands(vec![add, show, remove]);
601
602 let secret_key = Arg::with_name("secret-key").help("Mint authority secret key");
604
605 let token_blind = Arg::with_name("token-blind").help("Mint authority token blind");
606
607 let import = SubCommand::with_name("import")
608 .about("Import a mint authority")
609 .args(&[secret_key, token_blind]);
610
611 let generate_mint =
612 SubCommand::with_name("generate-mint").about("Generate a new mint authority");
613
614 let list =
615 SubCommand::with_name("list").about("List token IDs with available mint authorities");
616
617 let token = Arg::with_name("token").help("Token ID to mint");
618
619 let amount = Arg::with_name("amount").help("Amount to mint");
620
621 let recipient = Arg::with_name("recipient").help("Recipient of the minted tokens");
622
623 let mint = SubCommand::with_name("mint")
624 .about("Mint tokens")
625 .args(&[token, amount, recipient, spend_hook, user_data]);
626
627 let token = Arg::with_name("token").help("Token ID to freeze");
628
629 let freeze = SubCommand::with_name("freeze").about("Freeze a token mint").arg(token);
630
631 let token = SubCommand::with_name("token").about("Token functionalities").subcommands(vec![
632 import,
633 generate_mint,
634 list,
635 mint,
636 freeze,
637 ]);
638
639 let generate_deploy =
641 SubCommand::with_name("generate-deploy").about("Generate a new deploy authority");
642
643 let contract_id = Arg::with_name("contract-id").help("Contract ID (optional)");
644
645 let list = SubCommand::with_name("list")
646 .about("List deploy authorities in the wallet (or a specific one)")
647 .args(&[contract_id]);
648
649 let tx_hash = Arg::with_name("tx-hash").help("Record transaction hash");
650
651 let export_data = SubCommand::with_name("export-data")
652 .about("Export a contract history record wasm bincode and deployment instruction, encoded to base64")
653 .args(&[tx_hash]);
654
655 let deploy_auth = Arg::with_name("deploy-auth").help("Contract ID (deploy authority)");
656
657 let wasm_path = Arg::with_name("wasm-path").help("Path to contract wasm bincode");
658
659 let deploy_ix =
660 Arg::with_name("deploy-ix").help("Optional path to serialized deploy instruction");
661
662 let deploy = SubCommand::with_name("deploy").about("Deploy a smart contract").args(&[
663 deploy_auth.clone(),
664 wasm_path,
665 deploy_ix,
666 ]);
667
668 let lock = SubCommand::with_name("lock").about("Lock a smart contract").args(&[deploy_auth]);
669
670 let contract = SubCommand::with_name("contract")
671 .about("Contract functionalities")
672 .subcommands(vec![generate_deploy, list, export_data, deploy, lock]);
673
674 let config = Arg::with_name("config")
676 .short("c")
677 .long("config")
678 .takes_value(true)
679 .help("Configuration file to use");
680
681 let network = Arg::with_name("network")
682 .long("network")
683 .takes_value(true)
684 .help("Blockchain network to use");
685
686 let command = vec![
687 interactive,
688 kaching,
689 ping,
690 completions,
691 wallet,
692 spend,
693 unspend,
694 transfer,
695 otc,
696 attach_fee,
697 tx_from_calls,
698 inspect,
699 broadcast,
700 dao,
701 scan,
702 explorer,
703 alias,
704 token,
705 contract,
706 ];
707
708 let fun = Arg::with_name("fun")
709 .short("f")
710 .long("fun")
711 .help("Flag indicating whether you want some fun in your life");
712
713 let log = Arg::with_name("log")
714 .short("l")
715 .long("log")
716 .takes_value(true)
717 .help("Set log file to ouput into");
718
719 let verbose = Arg::with_name("verbose")
720 .short("v")
721 .multiple(true)
722 .help("Increase verbosity (-vvv supported)");
723
724 let mut app = App::new("drk")
725 .about(cli_desc!())
726 .args(&[config, network, fun, log, verbose])
727 .subcommands(command);
728
729 let shell = match Shell::from_str(shell) {
730 Ok(s) => s,
731 Err(e) => return Err(Error::Custom(e)),
732 };
733
734 let mut buf = vec![];
735 app.gen_completions_to("./drk", shell, &mut buf);
736
737 Ok(String::from_utf8(buf)?)
738}
739
740pub fn print_output(buf: &[String]) {
742 for line in buf {
743 println!("{line}");
744 }
745}
746
747pub async fn append_or_print(
751 buf: &mut Vec<String>,
752 sender: Option<&Sender<Vec<String>>>,
753 print: &bool,
754 messages: Vec<String>,
755) {
756 if let Some(sender) = sender {
758 if let Err(e) = sender.send(messages).await {
759 let err_msg = format!("[append_or_print] Sending messages to channel failed: {e}");
760 if *print {
761 println!("{err_msg}");
762 } else {
763 buf.push(err_msg);
764 }
765 }
766 return
767 }
768
769 if *print {
771 for msg in messages {
772 println!("{msg}");
773 }
774 return
775 }
776
777 for msg in messages {
779 buf.push(msg);
780 }
781}
782
783pub async fn parse_mining_config_from_stdin(
786) -> Result<(String, String, Option<String>, Option<String>)> {
787 let mut buf = String::new();
788 stdin().read_to_string(&mut buf)?;
789 let config = buf.trim();
790 let (recipient, spend_hook, user_data) = match base64::decode(config) {
791 Some(bytes) => deserialize_async(&bytes).await?,
792 None => return Err(Error::ParseFailed("Failed to decode mining configuration")),
793 };
794 Ok((config.to_string(), recipient, spend_hook, user_data))
795}
796
797pub async fn parse_mining_config_from_input(
800 input: &[String],
801) -> Result<(String, String, Option<String>, Option<String>)> {
802 match input.len() {
803 0 => parse_mining_config_from_stdin().await,
804 1 => {
805 let config = input[0].trim();
806 let (recipient, spend_hook, user_data) = match base64::decode(config) {
807 Some(bytes) => deserialize_async(&bytes).await?,
808 None => return Err(Error::ParseFailed("Failed to decode mining configuration")),
809 };
810 Ok((config.to_string(), recipient, spend_hook, user_data))
811 }
812 _ => Err(Error::ParseFailed("Multiline input provided")),
813 }
814}
815
816pub fn display_mining_config(
818 config: &str,
819 recipient_str: &str,
820 spend_hook: &Option<String>,
821 user_data: &Option<String>,
822 output: &mut Vec<String>,
823) {
824 output.push(format!("DarkFi mining configuration address: {config}"));
825
826 match Address::from_str(recipient_str) {
827 Ok(recipient) => {
828 output.push(format!("Recipient: {recipient_str}"));
829 output.push(format!("Public key: {}", recipient.public_key()));
830 output.push(format!("Network: {:?}", recipient.network()));
831 }
832 Err(e) => output.push(format!("Recipient: Invalid ({e})")),
833 }
834
835 let spend_hook = match spend_hook {
836 Some(spend_hook_str) => match FuncId::from_str(spend_hook_str) {
837 Ok(_) => String::from(spend_hook_str),
838 Err(e) => format!("Invalid ({e})"),
839 },
840 None => String::from("-"),
841 };
842 output.push(format!("Spend hook: {spend_hook}"));
843
844 let user_data = match user_data {
845 Some(user_data_str) => match bs58::decode(&user_data_str).into_vec() {
846 Ok(bytes) => match bytes.try_into() {
847 Ok(bytes) => {
848 if pallas::Base::from_repr(bytes).is_some().into() {
849 String::from(user_data_str)
850 } else {
851 String::from("Invalid")
852 }
853 }
854 Err(e) => format!("Invalid ({e:?})"),
855 },
856 Err(e) => format!("Invalid ({e})"),
857 },
858 None => String::from("-"),
859 };
860 output.push(format!("User data: {user_data}"));
861}
862
863fn to_leaf(call: &ContractCallImport) -> ContractCallLeaf {
865 ContractCallLeaf {
866 call: call.call().clone(),
867 proofs: call.proofs().iter().map(|p| Proof::new(p.clone())).collect(),
868 }
869}
870
871fn build_subtree(
873 idx: usize,
874 calls: &[ContractCallImport],
875 children_map: &HashMap<usize, &Vec<usize>>,
876) -> DarkTree<ContractCallLeaf> {
877 let children_idx = children_map.get(&idx).map(|v| v.as_slice()).unwrap_or(&[]);
878
879 let children: Vec<DarkTree<ContractCallLeaf>> =
880 children_idx.iter().map(|&i| build_subtree(i, calls, children_map)).collect();
881
882 DarkTree::new(to_leaf(&calls[idx]), children, None, None)
883}
884
885fn retrieve_signature_keys(
887 idx: usize,
888 calls: &[ContractCallImport],
889 children_map: &HashMap<usize, &Vec<usize>>,
890 sig_keys: &mut Vec<Vec<SecretKey>>,
891) {
892 let children_idx = children_map.get(&idx).map(|v| v.as_slice()).unwrap_or(&[]);
893
894 for i in children_idx {
895 retrieve_signature_keys(*i, calls, children_map, sig_keys)
896 }
897
898 sig_keys.push(calls[idx].secrets().to_vec());
899}
900
901pub fn tx_from_calls_mapped(
903 calls: &[ContractCallImport],
904 map: &[(usize, Vec<usize>)],
905) -> Result<(TransactionBuilder, Vec<Vec<SecretKey>>)> {
906 assert_eq!(calls.len(), map.len());
907
908 let children_map: HashMap<usize, &Vec<usize>> = map.iter().map(|(k, v)| (*k, v)).collect();
909 let all_children_idx: HashSet<&usize> = children_map.values().flat_map(|v| *v).collect();
910 let root_idxs: Vec<usize> =
911 map.iter().map(|(k, _)| *k).filter(|k| !all_children_idx.contains(k)).collect();
912
913 let root_idx = root_idxs[0];
915 let root_children: Vec<DarkTree<ContractCallLeaf>> =
916 children_map[&root_idx].iter().map(|&i| build_subtree(i, calls, &children_map)).collect();
917 let mut tx_builder = TransactionBuilder::new(to_leaf(&calls[root_idx]), root_children)?;
918
919 for root_idx in &root_idxs[1..] {
921 let root_children: Vec<DarkTree<ContractCallLeaf>> = children_map[root_idx]
922 .iter()
923 .map(|&i| build_subtree(i, calls, &children_map))
924 .collect();
925 tx_builder.append(to_leaf(&calls[*root_idx]), root_children)?;
926 }
927
928 let mut signature_secrets: Vec<Vec<SecretKey>> = vec![];
929 for idx in root_idxs {
930 retrieve_signature_keys(idx, calls, &children_map, &mut signature_secrets);
931 }
932
933 Ok((tx_builder, signature_secrets))
934}
935
936pub fn parse_tree(input: &str) -> std::result::Result<Vec<(usize, Vec<usize>)>, String> {
943 let s = input
944 .trim()
945 .strip_prefix('{')
946 .and_then(|s| s.strip_suffix('}'))
947 .ok_or("expected {}")?
948 .trim();
949
950 let mut entries = vec![];
951 let mut seen_keys = HashSet::new();
952
953 if s.is_empty() {
954 return Ok(entries)
955 }
956
957 let mut rest = s;
958 while !rest.is_empty() {
959 let (key_str, after_key) = rest.split_once(':').ok_or("expected ':'")?;
961 let key: usize = key_str.trim().parse().map_err(|_| "invalid key")?;
962
963 if !seen_keys.insert(key) {
964 return Err(format!("duplicate key: {}", key));
965 }
966
967 let after_key = after_key.trim();
969 let arr_start = after_key.strip_prefix('[').ok_or("expected '['")?;
970 let (arr_content, after_arr) = arr_start.split_once(']').ok_or("expected ']'")?;
971
972 let children: Vec<usize> = arr_content
973 .split(',')
974 .map(|s| s.trim())
975 .filter(|s| !s.is_empty())
976 .map(|s| s.parse().map_err(|_| "invalid child"))
977 .collect::<std::result::Result<_, _>>()?;
978
979 entries.push((key, children));
980
981 rest = after_arr.trim().strip_prefix(',').unwrap_or(after_arr).trim();
983 }
984
985 check_cycles(&entries)?;
986
987 Ok(entries)
988}
989
990fn check_cycles(entries: &[(usize, Vec<usize>)]) -> std::result::Result<(), String> {
991 let graph: HashMap<usize, &Vec<usize>> = entries.iter().map(|(k, v)| (*k, v)).collect();
992 let mut visited = HashSet::new();
993 let mut path = Vec::new();
994
995 fn dfs(
996 node: usize,
997 graph: &HashMap<usize, &Vec<usize>>,
998 visited: &mut HashSet<usize>,
999 path: &mut Vec<usize>,
1000 ) -> std::result::Result<(), String> {
1001 if let Some(pos) = path.iter().position(|&n| n == node) {
1002 let cycle: Vec<_> = path[pos..].iter().chain(&[node]).map(|n| n.to_string()).collect();
1003 return Err(format!("cycle detected: {}", cycle.join(" -> ")));
1004 }
1005
1006 if visited.contains(&node) {
1007 return Ok(());
1008 }
1009
1010 path.push(node);
1011 if let Some(children) = graph.get(&node) {
1012 for &child in *children {
1013 dfs(child, graph, visited, path)?;
1014 }
1015 }
1016 path.pop();
1017 visited.insert(node);
1018
1019 Ok(())
1020 }
1021
1022 for &(key, _) in entries {
1023 dfs(key, &graph, &mut visited, &mut path)?;
1024 }
1025
1026 Ok(())
1027}
1028
1029#[cfg(test)]
1030mod tests {
1031 use super::*;
1032 use darkfi_sdk::{
1033 crypto::{pasta_prelude::Field, ContractId},
1034 ContractCall,
1035 };
1036 use rand::rngs::OsRng;
1037
1038 #[test]
1039 fn test_parse_tree() {
1040 assert_eq!(parse_tree("{}").unwrap(), vec![]);
1042 assert_eq!(parse_tree("{ }").unwrap(), vec![]);
1043 assert_eq!(parse_tree("{ 0: [] }").unwrap(), vec![(0, vec![])]);
1044 assert_eq!(parse_tree("{ 0: [1, 2, 3] }").unwrap(), vec![(0, vec![1, 2, 3])]);
1045 assert_eq!(parse_tree("{0:[],1:[2]}").unwrap(), vec![(0, vec![]), (1, vec![2])]);
1046 assert_eq!(parse_tree("{ 0: [], 1: [], }").unwrap(), vec![(0, vec![]), (1, vec![])]);
1047 assert_eq!(parse_tree("{ 0: [1, 2,] }").unwrap(), vec![(0, vec![1, 2])]);
1048
1049 assert_eq!(
1050 parse_tree("{ 0: [], 1: [2, 3], 2: [], 3: [4], 4: [] }").unwrap(),
1051 vec![(0, vec![]), (1, vec![2, 3]), (2, vec![]), (3, vec![4]), (4, vec![])]
1052 );
1053
1054 assert_eq!(
1055 parse_tree("{ 0 : [ ] , 1 : [ 2 , 3 ] }").unwrap(),
1056 vec![(0, vec![]), (1, vec![2, 3])]
1057 );
1058
1059 assert_eq!(
1060 parse_tree("{ 999: [1000, 1001], 1000: [], 1001: [] }").unwrap(),
1061 vec![(999, vec![1000, 1001]), (1000, vec![]), (1001, vec![])]
1062 );
1063
1064 let keys: Vec<usize> =
1066 parse_tree("{ 5: [], 2: [], 9: [], 0: [] }").unwrap().iter().map(|(k, _)| *k).collect();
1067 assert_eq!(keys, vec![5, 2, 9, 0]);
1068
1069 assert!(parse_tree("{ 0: [1, 2], 1: [3], 2: [3], 3: [] }").is_ok());
1071
1072 assert!(parse_tree("0: [] }").is_err());
1074 assert!(parse_tree("{ 0: []").is_err());
1075 assert!(parse_tree("{ 0 [] }").is_err());
1076 assert!(parse_tree("{ 0: ] }").is_err());
1077 assert!(parse_tree("{ 0: [1, 2 }").is_err());
1078 assert!(parse_tree("{ abc: [] }").is_err());
1079 assert!(parse_tree("{ 0: [abc] }").is_err());
1080 assert!(parse_tree("{ -1: [] }").is_err());
1081
1082 assert!(parse_tree("{ 0: [], 0: [1] }").unwrap_err().contains("duplicate key: 0"));
1084 assert!(parse_tree("{ 0: [], 1: [], 2: [], 1: [] }")
1085 .unwrap_err()
1086 .contains("duplicate key: 1"));
1087
1088 let err = parse_tree("{ 0: [0] }").unwrap_err();
1090 assert!(err.contains("cycle detected") && err.contains("0 -> 0"));
1091
1092 let err = parse_tree("{ 0: [1], 1: [0] }").unwrap_err();
1093 assert!(err.contains("cycle detected"));
1094
1095 let err = parse_tree("{ 0: [1], 1: [2], 2: [3], 3: [0] }").unwrap_err();
1096 assert!(err.contains("cycle detected") && err.contains("0 -> 1 -> 2 -> 3 -> 0"));
1097
1098 let err = parse_tree("{ 0: [1], 1: [2], 2: [3], 3: [2] }").unwrap_err();
1099 assert!(err.contains("cycle detected") && err.contains("2 -> 3 -> 2"));
1100 }
1101
1102 #[test]
1103 fn test_tx_from_calls_mapped() {
1104 let contract0 = ContractId::from(pallas::Base::random(&mut OsRng));
1105 let contract1 = ContractId::from(pallas::Base::random(&mut OsRng));
1106 let contract2 = ContractId::from(pallas::Base::random(&mut OsRng));
1107 let call0 = ContractCallImport::new(
1108 ContractCall { contract_id: contract0, data: vec![] },
1109 vec![],
1110 vec![],
1111 );
1112 let call1 = ContractCallImport::new(
1113 ContractCall { contract_id: contract1, data: vec![] },
1114 vec![],
1115 vec![SecretKey::random(&mut OsRng), SecretKey::random(&mut OsRng)],
1116 );
1117 let call2 = ContractCallImport::new(
1118 ContractCall { contract_id: contract2, data: vec![] },
1119 vec![],
1120 vec![SecretKey::random(&mut OsRng)],
1121 );
1122
1123 let (mut tx_builder, sig_keys) = tx_from_calls_mapped(
1125 &[call0.clone(), call1.clone(), call2.clone()],
1126 &parse_tree("{0 : [], 1: [], 2: []}").unwrap(),
1127 )
1128 .unwrap();
1129 let leafs = tx_builder.calls.build_vec().unwrap();
1130
1131 assert_eq!(leafs.len(), 3);
1132 assert_eq!(leafs[0].data.call.contract_id, contract0);
1133 assert_eq!(leafs[1].data.call.contract_id, contract1);
1134 assert_eq!(leafs[2].data.call.contract_id, contract2);
1135 assert_eq!(sig_keys.len(), 3);
1136 assert_eq!(sig_keys[0].len(), 0);
1137 assert_eq!(sig_keys[1].len(), 2);
1138 assert_eq!(sig_keys[2].len(), 1);
1139
1140 let (mut tx_builder, sig_keys) = tx_from_calls_mapped(
1142 &[call0.clone(), call1.clone(), call2.clone()],
1143 &parse_tree("{0 : [1], 1: [], 2: []}").unwrap(),
1144 )
1145 .unwrap();
1146 let leafs = tx_builder.calls.build_vec().unwrap();
1147
1148 assert_eq!(leafs.len(), 3);
1149 assert_eq!(leafs[0].data.call.contract_id, contract1);
1150 assert_eq!(leafs[1].data.call.contract_id, contract0);
1151 assert_eq!(leafs[2].data.call.contract_id, contract2);
1152 assert_eq!(sig_keys.len(), 3);
1153 assert_eq!(sig_keys[0].len(), 2);
1154 assert_eq!(sig_keys[1].len(), 0);
1155 assert_eq!(sig_keys[2].len(), 1);
1156
1157 let (mut tx_builder, sig_keys) = tx_from_calls_mapped(
1159 &[call0.clone(), call1.clone(), call2.clone()],
1160 &parse_tree("{0 : [1, 2], 1: [], 2: []}").unwrap(),
1161 )
1162 .unwrap();
1163 let leafs = tx_builder.calls.build_vec().unwrap();
1164
1165 assert_eq!(leafs.len(), 3);
1166 assert_eq!(leafs[0].data.call.contract_id, contract1);
1167 assert_eq!(leafs[1].data.call.contract_id, contract2);
1168 assert_eq!(leafs[2].data.call.contract_id, contract0);
1169 assert_eq!(sig_keys.len(), 3);
1170 assert_eq!(sig_keys[0].len(), 2);
1171 assert_eq!(sig_keys[1].len(), 1);
1172 assert_eq!(sig_keys[2].len(), 0);
1173
1174 let (mut tx_builder, sig_keys) = tx_from_calls_mapped(
1177 &[call0, call1, call2],
1178 &parse_tree("{0 : [], 1: [0], 2: [1]}").unwrap(),
1179 )
1180 .unwrap();
1181 let leafs = tx_builder.calls.build_vec().unwrap();
1182
1183 assert_eq!(leafs.len(), 3);
1184 assert_eq!(leafs[0].data.call.contract_id, contract0);
1185 assert_eq!(leafs[1].data.call.contract_id, contract1);
1186 assert_eq!(leafs[2].data.call.contract_id, contract2);
1187 assert_eq!(sig_keys.len(), 3);
1188 assert_eq!(sig_keys[0].len(), 0);
1189 assert_eq!(sig_keys[1].len(), 2);
1190 assert_eq!(sig_keys[2].len(), 1);
1191 }
1192}