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;
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
48pub 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
58pub 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
71pub 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
84pub 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
101pub 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
118pub 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
141pub 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
155pub fn generate_completions(shell: &str) -> Result<String> {
157 let interactive = SubCommand::with_name("interactive").about("Enter Drk interactive shell");
161
162 let kaching = SubCommand::with_name("kaching").about("Fun");
164
165 let ping =
167 SubCommand::with_name("ping").about("Send a ping request to the darkfid RPC endpoint");
168
169 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 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 let spend = SubCommand::with_name("spend")
228 .about("Read a transaction from stdin and mark its input coins as spent");
229
230 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 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 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 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 opt_name = Arg::with_name("dao-alias").help("Name identifier for the DAO (optional)");
316
317 let list = SubCommand::with_name("list")
318 .about("List imported DAOs (or info about a specific one)")
319 .args(&[opt_name]);
320
321 let balance = SubCommand::with_name("balance")
322 .about("Show the balance of a DAO")
323 .args(slice::from_ref(&name));
324
325 let mint = SubCommand::with_name("mint")
326 .about("Mint an imported DAO on-chain")
327 .args(slice::from_ref(&name));
328
329 let duration = Arg::with_name("duration").help("Duration of the proposal, in block windows");
330
331 let propose_transfer = SubCommand::with_name("propose-transfer")
332 .about("Create a transfer proposal for a DAO")
333 .args(&[
334 name.clone(),
335 duration.clone(),
336 amount,
337 token,
338 recipient,
339 spend_hook.clone(),
340 user_data.clone(),
341 ]);
342
343 let propose_generic = SubCommand::with_name("propose-generic")
344 .about("Create a generic proposal for a DAO")
345 .args(&[name.clone(), duration, user_data.clone()]);
346
347 let proposals = SubCommand::with_name("proposals").about("List DAO proposals").arg(&name);
348
349 let bulla = Arg::with_name("bulla").help("Bulla identifier for the proposal");
350
351 let export = Arg::with_name("export").help("Encrypt the proposal and encode it to base64");
352
353 let mint_proposal = Arg::with_name("mint-proposal").help("Create the proposal transaction");
354
355 let proposal = SubCommand::with_name("proposal").about("View a DAO proposal data").args(&[
356 bulla.clone(),
357 export,
358 mint_proposal,
359 ]);
360
361 let proposal_import = SubCommand::with_name("proposal-import")
362 .about("Import a base64 encoded and encrypted proposal from stdin");
363
364 let vote = Arg::with_name("vote").help("Vote (0 for NO, 1 for YES)");
365
366 let vote_weight =
367 Arg::with_name("vote-weight").help("Optional vote weight (amount of governance tokens)");
368
369 let vote = SubCommand::with_name("vote").about("Vote on a given proposal").args(&[
370 bulla.clone(),
371 vote,
372 vote_weight,
373 ]);
374
375 let early = Arg::with_name("early").long("early").help("Execute the proposal early");
376
377 let exec = SubCommand::with_name("exec").about("Execute a DAO proposal").args(&[bulla, early]);
378
379 let spend_hook_cmd = SubCommand::with_name("spend-hook")
380 .about("Print the DAO contract base64-encoded spend hook");
381
382 let mining_config =
383 SubCommand::with_name("mining-config").about("Print a DAO mining configuration").arg(name);
384
385 let dao = SubCommand::with_name("dao").about("DAO functionalities").subcommands(vec![
386 create,
387 view,
388 import,
389 list,
390 balance,
391 mint,
392 propose_transfer,
393 propose_generic,
394 proposals,
395 proposal,
396 proposal_import,
397 vote,
398 exec,
399 spend_hook_cmd,
400 mining_config,
401 ]);
402
403 let attach_fee = SubCommand::with_name("attach-fee")
405 .about("Attach the fee call to a transaction given from stdin");
406
407 let calls_map =
409 Arg::with_name("calls-map").help("Optional parent/children dependency map for the calls");
410
411 let tx_from_calls = SubCommand::with_name("tx-from-calls")
412 .about("Create a transaction from newline-separated calls from stdin")
413 .args(&[calls_map]);
414
415 let inspect = SubCommand::with_name("inspect").about("Inspect a transaction from stdin");
417
418 let broadcast =
420 SubCommand::with_name("broadcast").about("Read a transaction from stdin and broadcast it");
421
422 let reset = Arg::with_name("reset")
424 .long("reset")
425 .help("Reset wallet state to provided block height and start scanning");
426
427 let scan = SubCommand::with_name("scan")
428 .about("Scan the blockchain and parse relevant transactions")
429 .args(&[reset]);
430
431 let tx_hash = Arg::with_name("tx-hash").help("Transaction hash");
433
434 let encode = Arg::with_name("encode").long("encode").help("Encode transaction to base64");
435
436 let fetch_tx = SubCommand::with_name("fetch-tx")
437 .about("Fetch a blockchain transaction by hash")
438 .args(&[tx_hash, encode]);
439
440 let simulate_tx =
441 SubCommand::with_name("simulate-tx").about("Read a transaction from stdin and simulate it");
442
443 let tx_hash = Arg::with_name("tx-hash").help("Fetch specific history record (optional)");
444
445 let encode = Arg::with_name("encode")
446 .long("encode")
447 .help("Encode specific history record transaction to base64");
448
449 let txs_history = SubCommand::with_name("txs-history")
450 .about("Fetch broadcasted transactions history")
451 .args(&[tx_hash, encode]);
452
453 let clear_reverted =
454 SubCommand::with_name("clear-reverted").about("Remove reverted transactions from history");
455
456 let height = Arg::with_name("height").help("Fetch specific height record (optional)");
457
458 let scanned_blocks = SubCommand::with_name("scanned-blocks")
459 .about("Fetch scanned blocks records")
460 .args(&[height]);
461
462 let mining_config = SubCommand::with_name("mining-config")
463 .about("Read a mining configuration from stdin and display its parts");
464
465 let explorer =
466 SubCommand::with_name("explorer").about("Explorer related subcommands").subcommands(vec![
467 fetch_tx,
468 simulate_tx,
469 txs_history,
470 clear_reverted,
471 scanned_blocks,
472 mining_config,
473 ]);
474
475 let alias = Arg::with_name("alias").help("Token alias");
477
478 let token = Arg::with_name("token").help("Token to create alias for");
479
480 let add = SubCommand::with_name("add").about("Create a Token alias").args(&[alias, token]);
481
482 let alias = Arg::with_name("alias")
483 .short("a")
484 .long("alias")
485 .takes_value(true)
486 .help("Token alias to search for");
487
488 let token = Arg::with_name("token")
489 .short("t")
490 .long("token")
491 .takes_value(true)
492 .help("Token to search alias for");
493
494 let show = SubCommand::with_name("show")
495 .about(
496 "Print alias info of optional arguments. \
497 If no argument is provided, list all the aliases in the wallet.",
498 )
499 .args(&[alias, token]);
500
501 let alias = Arg::with_name("alias").help("Token alias to remove");
502
503 let remove = SubCommand::with_name("remove").about("Remove a Token alias").arg(alias);
504
505 let alias = SubCommand::with_name("alias")
506 .about("Manage Token aliases")
507 .subcommands(vec![add, show, remove]);
508
509 let secret_key = Arg::with_name("secret-key").help("Mint authority secret key");
511
512 let token_blind = Arg::with_name("token-blind").help("Mint authority token blind");
513
514 let import = SubCommand::with_name("import")
515 .about("Import a mint authority")
516 .args(&[secret_key, token_blind]);
517
518 let generate_mint =
519 SubCommand::with_name("generate-mint").about("Generate a new mint authority");
520
521 let list =
522 SubCommand::with_name("list").about("List token IDs with available mint authorities");
523
524 let token = Arg::with_name("token").help("Token ID to mint");
525
526 let amount = Arg::with_name("amount").help("Amount to mint");
527
528 let recipient = Arg::with_name("recipient").help("Recipient of the minted tokens");
529
530 let mint = SubCommand::with_name("mint")
531 .about("Mint tokens")
532 .args(&[token, amount, recipient, spend_hook, user_data]);
533
534 let token = Arg::with_name("token").help("Token ID to freeze");
535
536 let freeze = SubCommand::with_name("freeze").about("Freeze a token mint").arg(token);
537
538 let token = SubCommand::with_name("token").about("Token functionalities").subcommands(vec![
539 import,
540 generate_mint,
541 list,
542 mint,
543 freeze,
544 ]);
545
546 let generate_deploy =
548 SubCommand::with_name("generate-deploy").about("Generate a new deploy authority");
549
550 let contract_id = Arg::with_name("contract-id").help("Contract ID (optional)");
551
552 let list = SubCommand::with_name("list")
553 .about("List deploy authorities in the wallet (or a specific one)")
554 .args(&[contract_id]);
555
556 let tx_hash = Arg::with_name("tx-hash").help("Record transaction hash");
557
558 let export_data = SubCommand::with_name("export-data")
559 .about("Export a contract history record wasm bincode and deployment instruction, encoded to base64")
560 .args(&[tx_hash]);
561
562 let deploy_auth = Arg::with_name("deploy-auth").help("Contract ID (deploy authority)");
563
564 let wasm_path = Arg::with_name("wasm-path").help("Path to contract wasm bincode");
565
566 let deploy_ix =
567 Arg::with_name("deploy-ix").help("Optional path to serialized deploy instruction");
568
569 let deploy = SubCommand::with_name("deploy").about("Deploy a smart contract").args(&[
570 deploy_auth.clone(),
571 wasm_path,
572 deploy_ix,
573 ]);
574
575 let lock = SubCommand::with_name("lock").about("Lock a smart contract").args(&[deploy_auth]);
576
577 let contract = SubCommand::with_name("contract")
578 .about("Contract functionalities")
579 .subcommands(vec![generate_deploy, list, export_data, deploy, lock]);
580
581 let config = Arg::with_name("config")
583 .short("c")
584 .long("config")
585 .takes_value(true)
586 .help("Configuration file to use");
587
588 let network = Arg::with_name("network")
589 .long("network")
590 .takes_value(true)
591 .help("Blockchain network to use");
592
593 let command = vec![
594 interactive,
595 kaching,
596 ping,
597 completions,
598 wallet,
599 spend,
600 unspend,
601 transfer,
602 otc,
603 attach_fee,
604 tx_from_calls,
605 inspect,
606 broadcast,
607 dao,
608 scan,
609 explorer,
610 alias,
611 token,
612 contract,
613 ];
614
615 let fun = Arg::with_name("fun")
616 .short("f")
617 .long("fun")
618 .help("Flag indicating whether you want some fun in your life");
619
620 let log = Arg::with_name("log")
621 .short("l")
622 .long("log")
623 .takes_value(true)
624 .help("Set log file to ouput into");
625
626 let verbose = Arg::with_name("verbose")
627 .short("v")
628 .multiple(true)
629 .help("Increase verbosity (-vvv supported)");
630
631 let mut app = App::new("drk")
632 .about(cli_desc!())
633 .args(&[config, network, fun, log, verbose])
634 .subcommands(command);
635
636 let shell = match Shell::from_str(shell) {
637 Ok(s) => s,
638 Err(e) => return Err(Error::Custom(e)),
639 };
640
641 let mut buf = vec![];
642 app.gen_completions_to("./drk", shell, &mut buf);
643
644 Ok(String::from_utf8(buf)?)
645}
646
647pub fn print_output(buf: &[String]) {
649 for line in buf {
650 println!("{line}");
651 }
652}
653
654pub async fn append_or_print(
658 buf: &mut Vec<String>,
659 sender: Option<&Sender<Vec<String>>>,
660 print: &bool,
661 messages: Vec<String>,
662) {
663 if let Some(sender) = sender {
665 if let Err(e) = sender.send(messages).await {
666 let err_msg = format!("[append_or_print] Sending messages to channel failed: {e}");
667 if *print {
668 println!("{err_msg}");
669 } else {
670 buf.push(err_msg);
671 }
672 }
673 return
674 }
675
676 if *print {
678 for msg in messages {
679 println!("{msg}");
680 }
681 return
682 }
683
684 for msg in messages {
686 buf.push(msg);
687 }
688}
689
690pub async fn parse_mining_config_from_stdin(
693) -> Result<(String, String, Option<String>, Option<String>)> {
694 let mut buf = String::new();
695 stdin().read_to_string(&mut buf)?;
696 let config = buf.trim();
697 let (recipient, spend_hook, user_data) = match base64::decode(config) {
698 Some(bytes) => deserialize_async(&bytes).await?,
699 None => return Err(Error::ParseFailed("Failed to decode mining configuration")),
700 };
701 Ok((config.to_string(), recipient, spend_hook, user_data))
702}
703
704pub async fn parse_mining_config_from_input(
707 input: &[String],
708) -> Result<(String, String, Option<String>, Option<String>)> {
709 match input.len() {
710 0 => parse_mining_config_from_stdin().await,
711 1 => {
712 let config = input[0].trim();
713 let (recipient, spend_hook, user_data) = match base64::decode(config) {
714 Some(bytes) => deserialize_async(&bytes).await?,
715 None => return Err(Error::ParseFailed("Failed to decode mining configuration")),
716 };
717 Ok((config.to_string(), recipient, spend_hook, user_data))
718 }
719 _ => Err(Error::ParseFailed("Multiline input provided")),
720 }
721}
722
723pub fn display_mining_config(
725 config: &str,
726 recipient_str: &str,
727 spend_hook: &Option<String>,
728 user_data: &Option<String>,
729 output: &mut Vec<String>,
730) {
731 output.push(format!("DarkFi mining configuration address: {config}"));
732
733 match Address::from_str(recipient_str) {
734 Ok(recipient) => {
735 output.push(format!("Recipient: {recipient_str}"));
736 output.push(format!("Public key: {}", recipient.public_key()));
737 output.push(format!("Network: {:?}", recipient.network()));
738 }
739 Err(e) => output.push(format!("Recipient: Invalid ({e})")),
740 }
741
742 let spend_hook = match spend_hook {
743 Some(spend_hook_str) => match FuncId::from_str(spend_hook_str) {
744 Ok(_) => String::from(spend_hook_str),
745 Err(e) => format!("Invalid ({e})"),
746 },
747 None => String::from("-"),
748 };
749 output.push(format!("Spend hook: {spend_hook}"));
750
751 let user_data = match user_data {
752 Some(user_data_str) => match bs58::decode(&user_data_str).into_vec() {
753 Ok(bytes) => match bytes.try_into() {
754 Ok(bytes) => {
755 if pallas::Base::from_repr(bytes).is_some().into() {
756 String::from(user_data_str)
757 } else {
758 String::from("Invalid")
759 }
760 }
761 Err(e) => format!("Invalid ({e:?})"),
762 },
763 Err(e) => format!("Invalid ({e})"),
764 },
765 None => String::from("-"),
766 };
767 output.push(format!("User data: {user_data}"));
768}
769
770fn to_leaf(call: &ContractCallImport) -> ContractCallLeaf {
772 ContractCallLeaf {
773 call: call.call().clone(),
774 proofs: call.proofs().iter().map(|p| Proof::new(p.clone())).collect(),
775 }
776}
777
778fn build_subtree(
780 idx: usize,
781 calls: &[ContractCallImport],
782 children_map: &HashMap<usize, &Vec<usize>>,
783) -> DarkTree<ContractCallLeaf> {
784 let children_idx = children_map.get(&idx).map(|v| v.as_slice()).unwrap_or(&[]);
785
786 let children: Vec<DarkTree<ContractCallLeaf>> =
787 children_idx.iter().map(|&i| build_subtree(i, calls, children_map)).collect();
788
789 DarkTree::new(to_leaf(&calls[idx]), children, None, None)
790}
791
792pub fn tx_from_calls_mapped(
794 calls: &[ContractCallImport],
795 map: &[(usize, Vec<usize>)],
796) -> Result<(TransactionBuilder, Vec<SecretKey>)> {
797 assert_eq!(calls.len(), map.len());
798
799 let signature_secrets: Vec<SecretKey> =
800 calls.iter().flat_map(|c| c.secrets().to_vec()).collect();
801
802 let children_map: HashMap<usize, &Vec<usize>> = map.iter().map(|(k, v)| (*k, v)).collect();
803
804 let (root_idx, root_children_idx) = &map[0];
805
806 let root_children: Vec<DarkTree<ContractCallLeaf>> =
807 root_children_idx.iter().map(|&i| build_subtree(i, calls, &children_map)).collect();
808
809 let tx_builder = TransactionBuilder::new(to_leaf(&calls[*root_idx]), root_children)?;
810
811 Ok((tx_builder, signature_secrets))
812}
813
814pub fn parse_tree(input: &str) -> std::result::Result<Vec<(usize, Vec<usize>)>, String> {
821 let s = input
822 .trim()
823 .strip_prefix('{')
824 .and_then(|s| s.strip_suffix('}'))
825 .ok_or("expected {}")?
826 .trim();
827
828 let mut entries = vec![];
829 let mut seen_keys = HashSet::new();
830
831 if s.is_empty() {
832 return Ok(entries)
833 }
834
835 let mut rest = s;
836 while !rest.is_empty() {
837 let (key_str, after_key) = rest.split_once(':').ok_or("expected ':'")?;
839 let key: usize = key_str.trim().parse().map_err(|_| "invalid key")?;
840
841 if !seen_keys.insert(key) {
842 return Err(format!("duplicate key: {}", key));
843 }
844
845 let after_key = after_key.trim();
847 let arr_start = after_key.strip_prefix('[').ok_or("expected '['")?;
848 let (arr_content, after_arr) = arr_start.split_once(']').ok_or("expected ']'")?;
849
850 let children: Vec<usize> = arr_content
851 .split(',')
852 .map(|s| s.trim())
853 .filter(|s| !s.is_empty())
854 .map(|s| s.parse().map_err(|_| "invalid child"))
855 .collect::<std::result::Result<_, _>>()?;
856
857 entries.push((key, children));
858
859 rest = after_arr.trim().strip_prefix(',').unwrap_or(after_arr).trim();
861 }
862
863 check_cycles(&entries)?;
864
865 Ok(entries)
866}
867
868fn check_cycles(entries: &[(usize, Vec<usize>)]) -> std::result::Result<(), String> {
869 let graph: HashMap<usize, &Vec<usize>> = entries.iter().map(|(k, v)| (*k, v)).collect();
870 let mut visited = HashSet::new();
871 let mut path = Vec::new();
872
873 fn dfs(
874 node: usize,
875 graph: &HashMap<usize, &Vec<usize>>,
876 visited: &mut HashSet<usize>,
877 path: &mut Vec<usize>,
878 ) -> std::result::Result<(), String> {
879 if let Some(pos) = path.iter().position(|&n| n == node) {
880 let cycle: Vec<_> = path[pos..].iter().chain(&[node]).map(|n| n.to_string()).collect();
881 return Err(format!("cycle detected: {}", cycle.join(" -> ")));
882 }
883
884 if visited.contains(&node) {
885 return Ok(());
886 }
887
888 path.push(node);
889 if let Some(children) = graph.get(&node) {
890 for &child in *children {
891 dfs(child, graph, visited, path)?;
892 }
893 }
894 path.pop();
895 visited.insert(node);
896
897 Ok(())
898 }
899
900 for &(key, _) in entries {
901 dfs(key, &graph, &mut visited, &mut path)?;
902 }
903
904 Ok(())
905}
906
907#[cfg(test)]
908mod tests {
909 use super::*;
910
911 #[test]
912 fn test_parse_tree() {
913 assert_eq!(parse_tree("{}").unwrap(), vec![]);
915 assert_eq!(parse_tree("{ }").unwrap(), vec![]);
916 assert_eq!(parse_tree("{ 0: [] }").unwrap(), vec![(0, vec![])]);
917 assert_eq!(parse_tree("{ 0: [1, 2, 3] }").unwrap(), vec![(0, vec![1, 2, 3])]);
918 assert_eq!(parse_tree("{0:[],1:[2]}").unwrap(), vec![(0, vec![]), (1, vec![2])]);
919 assert_eq!(parse_tree("{ 0: [], 1: [], }").unwrap(), vec![(0, vec![]), (1, vec![])]);
920 assert_eq!(parse_tree("{ 0: [1, 2,] }").unwrap(), vec![(0, vec![1, 2])]);
921
922 assert_eq!(
923 parse_tree("{ 0: [], 1: [2, 3], 2: [], 3: [4], 4: [] }").unwrap(),
924 vec![(0, vec![]), (1, vec![2, 3]), (2, vec![]), (3, vec![4]), (4, vec![])]
925 );
926
927 assert_eq!(
928 parse_tree("{ 0 : [ ] , 1 : [ 2 , 3 ] }").unwrap(),
929 vec![(0, vec![]), (1, vec![2, 3])]
930 );
931
932 assert_eq!(
933 parse_tree("{ 999: [1000, 1001], 1000: [], 1001: [] }").unwrap(),
934 vec![(999, vec![1000, 1001]), (1000, vec![]), (1001, vec![])]
935 );
936
937 let keys: Vec<usize> =
939 parse_tree("{ 5: [], 2: [], 9: [], 0: [] }").unwrap().iter().map(|(k, _)| *k).collect();
940 assert_eq!(keys, vec![5, 2, 9, 0]);
941
942 assert!(parse_tree("{ 0: [1, 2], 1: [3], 2: [3], 3: [] }").is_ok());
944
945 assert!(parse_tree("0: [] }").is_err());
947 assert!(parse_tree("{ 0: []").is_err());
948 assert!(parse_tree("{ 0 [] }").is_err());
949 assert!(parse_tree("{ 0: ] }").is_err());
950 assert!(parse_tree("{ 0: [1, 2 }").is_err());
951 assert!(parse_tree("{ abc: [] }").is_err());
952 assert!(parse_tree("{ 0: [abc] }").is_err());
953 assert!(parse_tree("{ -1: [] }").is_err());
954
955 assert!(parse_tree("{ 0: [], 0: [1] }").unwrap_err().contains("duplicate key: 0"));
957 assert!(parse_tree("{ 0: [], 1: [], 2: [], 1: [] }")
958 .unwrap_err()
959 .contains("duplicate key: 1"));
960
961 let err = parse_tree("{ 0: [0] }").unwrap_err();
963 assert!(err.contains("cycle detected") && err.contains("0 -> 0"));
964
965 let err = parse_tree("{ 0: [1], 1: [0] }").unwrap_err();
966 assert!(err.contains("cycle detected"));
967
968 let err = parse_tree("{ 0: [1], 1: [2], 2: [3], 3: [0] }").unwrap_err();
969 assert!(err.contains("cycle detected") && err.contains("0 -> 1 -> 2 -> 3 -> 0"));
970
971 let err = parse_tree("{ 0: [1], 1: [2], 2: [3], 3: [2] }").unwrap_err();
972 assert!(err.contains("cycle detected") && err.contains("2 -> 3 -> 2"));
973 }
974}