cli_util.rs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618
  1. /* This file is part of DarkFi (https://dark.fi)
  2. *
  3. * Copyright (C) 2020-2025 Dyne.org foundation
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU Affero General Public License as
  7. * published by the Free Software Foundation, either version 3 of the
  8. * License, or (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU Affero General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU Affero General Public License
  16. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. */
  18. use std::{
  19. io::{stdin, Cursor, Read},
  20. str::FromStr,
  21. };
  22. use rodio::{Decoder, OutputStream, Sink};
  23. use smol::channel::Sender;
  24. use structopt_toml::clap::{App, Arg, Shell, SubCommand};
  25. use darkfi::{
  26. cli_desc,
  27. tx::Transaction,
  28. util::{encoding::base64, parse::decode_base10},
  29. Error, Result,
  30. };
  31. use darkfi_money_contract::model::TokenId;
  32. use darkfi_serial::deserialize_async;
  33. use crate::{money::BALANCE_BASE10_DECIMALS, Drk};
  34. /// Auxiliary function to parse a base64 encoded transaction from stdin.
  35. pub async fn parse_tx_from_stdin() -> Result<Transaction> {
  36. let mut buf = String::new();
  37. stdin().read_to_string(&mut buf)?;
  38. match base64::decode(buf.trim()) {
  39. Some(bytes) => Ok(deserialize_async(&bytes).await?),
  40. None => Err(Error::ParseFailed("Failed to decode transaction")),
  41. }
  42. }
  43. /// Auxiliary function to parse a base64 encoded transaction from
  44. /// provided input or fallback to stdin if its empty.
  45. pub async fn parse_tx_from_input(input: &[String]) -> Result<Transaction> {
  46. match input.len() {
  47. 0 => parse_tx_from_stdin().await,
  48. 1 => match base64::decode(input[0].trim()) {
  49. Some(bytes) => Ok(deserialize_async(&bytes).await?),
  50. None => Err(Error::ParseFailed("Failed to decode transaction")),
  51. },
  52. _ => Err(Error::ParseFailed("Multiline input provided")),
  53. }
  54. }
  55. /// Auxiliary function to parse provided string into a values pair.
  56. pub fn parse_value_pair(s: &str) -> Result<(u64, u64)> {
  57. let v: Vec<&str> = s.split(':').collect();
  58. if v.len() != 2 {
  59. return Err(Error::ParseFailed("Invalid value pair. Use a pair such as 13.37:11.0"))
  60. }
  61. let val0 = decode_base10(v[0], BALANCE_BASE10_DECIMALS, true);
  62. let val1 = decode_base10(v[1], BALANCE_BASE10_DECIMALS, true);
  63. if val0.is_err() || val1.is_err() {
  64. return Err(Error::ParseFailed("Invalid value pair. Use a pair such as 13.37:11.0"))
  65. }
  66. Ok((val0.unwrap(), val1.unwrap()))
  67. }
  68. /// Auxiliary function to parse provided string into a tokens pair.
  69. pub async fn parse_token_pair(drk: &Drk, s: &str) -> Result<(TokenId, TokenId)> {
  70. let v: Vec<&str> = s.split(':').collect();
  71. if v.len() != 2 {
  72. return Err(Error::ParseFailed(
  73. "Invalid token pair. Use a pair such as:\nWCKD:MLDY\nor\n\
  74. A7f1RKsCUUHrSXA7a9ogmwg8p3bs6F47ggsW826HD4yd:FCuoMii64H5Ee4eVWBjP18WTFS8iLUJmGi16Qti1xFQ2"
  75. ))
  76. }
  77. let tok0 = drk.get_token(v[0].to_string()).await;
  78. let tok1 = drk.get_token(v[1].to_string()).await;
  79. if tok0.is_err() || tok1.is_err() {
  80. return Err(Error::ParseFailed(
  81. "Invalid token pair. Use a pair such as:\nWCKD:MLDY\nor\n\
  82. A7f1RKsCUUHrSXA7a9ogmwg8p3bs6F47ggsW826HD4yd:FCuoMii64H5Ee4eVWBjP18WTFS8iLUJmGi16Qti1xFQ2"
  83. ))
  84. }
  85. Ok((tok0.unwrap(), tok1.unwrap()))
  86. }
  87. /// Fun police go away
  88. pub async fn kaching() {
  89. const WALLET_MP3: &[u8] = include_bytes!("../wallet.mp3");
  90. let cursor = Cursor::new(WALLET_MP3);
  91. let Ok((_stream, stream_handle)) = OutputStream::try_default() else { return };
  92. let Ok(sink) = Sink::try_new(&stream_handle) else { return };
  93. let Ok(source) = Decoder::new(cursor) else { return };
  94. sink.append(source);
  95. sink.sleep_until_end();
  96. }
  97. /// Auxiliary function to generate provided shell completions.
  98. pub fn generate_completions(shell: &str) -> Result<String> {
  99. // Sub-commands
  100. // Interactive
  101. let interactive = SubCommand::with_name("interactive").about("Enter Drk interactive shell");
  102. // Kaching
  103. let kaching = SubCommand::with_name("kaching").about("Fun");
  104. // Ping
  105. let ping =
  106. SubCommand::with_name("ping").about("Send a ping request to the darkfid RPC endpoint");
  107. // Completions
  108. let shell_arg = Arg::with_name("shell").help("The Shell you want to generate script for");
  109. let completions = SubCommand::with_name("completions")
  110. .about("Generate a SHELL completion script and print to stdout")
  111. .arg(shell_arg);
  112. // Wallet
  113. let initialize = SubCommand::with_name("initialize").about("Initialize wallet database");
  114. let keygen = SubCommand::with_name("keygen").about("Generate a new keypair in the wallet");
  115. let balance = SubCommand::with_name("balance").about("Query the wallet for known balances");
  116. let address = SubCommand::with_name("address").about("Get the default address in the wallet");
  117. let addresses =
  118. SubCommand::with_name("addresses").about("Print all the addresses in the wallet");
  119. let index = Arg::with_name("index").help("Identifier of the address");
  120. let default_address = SubCommand::with_name("default-address")
  121. .about("Set the default address in the wallet")
  122. .arg(index);
  123. let secrets =
  124. SubCommand::with_name("secrets").about("Print all the secret keys from the wallet");
  125. let import_secrets = SubCommand::with_name("import-secrets")
  126. .about("Import secret keys from stdin into the wallet, separated by newlines");
  127. let tree = SubCommand::with_name("tree").about("Print the Merkle tree in the wallet");
  128. let coins = SubCommand::with_name("coins").about("Print all the coins in the wallet");
  129. let wallet = SubCommand::with_name("wallet").about("Wallet operations").subcommands(vec![
  130. initialize,
  131. keygen,
  132. balance,
  133. address,
  134. addresses,
  135. default_address,
  136. secrets,
  137. import_secrets,
  138. tree,
  139. coins,
  140. ]);
  141. // Spend
  142. let spend = SubCommand::with_name("spend")
  143. .about("Read a transaction from stdin and mark its input coins as spent");
  144. // Unspend
  145. let coin = Arg::with_name("coin").help("base58-encoded coin to mark as unspent");
  146. let unspend = SubCommand::with_name("unspend").about("Unspend a coin").arg(coin);
  147. // Transfer
  148. let amount = Arg::with_name("amount").help("Amount to send");
  149. let token = Arg::with_name("token").help("Token ID to send");
  150. let recipient = Arg::with_name("recipient").help("Recipient address");
  151. let spend_hook = Arg::with_name("spend-hook").help("Optional contract spend hook to use");
  152. let user_data = Arg::with_name("user-data").help("Optional user data to use");
  153. let half_split = Arg::with_name("half-split")
  154. .long("half-split")
  155. .help("Split the output coin into two equal halves");
  156. let transfer =
  157. SubCommand::with_name("transfer").about("Create a payment transaction").args(&vec![
  158. amount.clone(),
  159. token.clone(),
  160. recipient.clone(),
  161. spend_hook.clone(),
  162. user_data.clone(),
  163. half_split,
  164. ]);
  165. // Otc
  166. let value_pair = Arg::with_name("value-pair")
  167. .short("v")
  168. .long("value-pair")
  169. .takes_value(true)
  170. .help("Value pair to send:recv (11.55:99.42)");
  171. let token_pair = Arg::with_name("token-pair")
  172. .short("t")
  173. .long("token-pair")
  174. .takes_value(true)
  175. .help("Token pair to send:recv (f00:b4r)");
  176. let init = SubCommand::with_name("init")
  177. .about("Initialize the first half of the atomic swap")
  178. .args(&vec![value_pair, token_pair]);
  179. let join =
  180. SubCommand::with_name("join").about("Build entire swap tx given the first half from stdin");
  181. let inspect = SubCommand::with_name("inspect")
  182. .about("Inspect a swap half or the full swap tx from stdin");
  183. let sign = SubCommand::with_name("sign").about("Sign a swap transaction given from stdin");
  184. let otc = SubCommand::with_name("otc")
  185. .about("OTC atomic swap")
  186. .subcommands(vec![init, join, inspect, sign]);
  187. // DAO
  188. let proposer_limit = Arg::with_name("proposer-limit")
  189. .help("The minimum amount of governance tokens needed to open a proposal for this DAO");
  190. let quorum = Arg::with_name("quorum")
  191. .help("Minimal threshold of participating total tokens needed for a proposal to pass");
  192. let early_exec_quorum = Arg::with_name("early-exec-quorum")
  193. .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.");
  194. let approval_ratio = Arg::with_name("approval-ratio")
  195. .help("The ratio of winning votes/total votes needed for a proposal to pass (2 decimals)");
  196. let gov_token_id = Arg::with_name("gov-token-id").help("DAO's governance token ID");
  197. let create = SubCommand::with_name("create").about("Create DAO parameters").args(&vec![
  198. proposer_limit,
  199. quorum,
  200. early_exec_quorum,
  201. approval_ratio,
  202. gov_token_id,
  203. ]);
  204. let view = SubCommand::with_name("view").about("View DAO data from stdin");
  205. let name = Arg::with_name("name").help("Name identifier for the DAO");
  206. let import = SubCommand::with_name("import")
  207. .about("Import DAO data from stdin")
  208. .args(&vec![name.clone()]);
  209. let update_keys = SubCommand::with_name("update-keys").about("Update DAO keys from stdin");
  210. let opt_name = Arg::with_name("dao-alias").help("Name identifier for the DAO (optional)");
  211. let list = SubCommand::with_name("list")
  212. .about("List imported DAOs (or info about a specific one)")
  213. .args(&vec![opt_name]);
  214. let balance = SubCommand::with_name("balance")
  215. .about("Show the balance of a DAO")
  216. .args(&vec![name.clone()]);
  217. let mint = SubCommand::with_name("mint")
  218. .about("Mint an imported DAO on-chain")
  219. .args(&vec![name.clone()]);
  220. let duration = Arg::with_name("duration").help("Duration of the proposal, in block windows");
  221. let propose_transfer = SubCommand::with_name("propose-transfer")
  222. .about("Create a transfer proposal for a DAO")
  223. .args(&vec![
  224. name.clone(),
  225. duration.clone(),
  226. amount,
  227. token,
  228. recipient,
  229. spend_hook.clone(),
  230. user_data.clone(),
  231. ]);
  232. let propose_generic = SubCommand::with_name("propose-generic")
  233. .about("Create a generic proposal for a DAO")
  234. .args(&vec![name.clone(), duration, user_data.clone()]);
  235. let proposals =
  236. SubCommand::with_name("proposals").about("List DAO proposals").args(&vec![name]);
  237. let bulla = Arg::with_name("bulla").help("Bulla identifier for the proposal");
  238. let export = Arg::with_name("export").help("Encrypt the proposal and encode it to base64");
  239. let mint_proposal = Arg::with_name("mint-proposal").help("Create the proposal transaction");
  240. let proposal = SubCommand::with_name("proposal").about("View a DAO proposal data").args(&vec![
  241. bulla.clone(),
  242. export,
  243. mint_proposal,
  244. ]);
  245. let proposal_import = SubCommand::with_name("proposal-import")
  246. .about("Import a base64 encoded and encrypted proposal from stdin");
  247. let vote = Arg::with_name("vote").help("Vote (0 for NO, 1 for YES)");
  248. let vote_weight =
  249. Arg::with_name("vote-weight").help("Optional vote weight (amount of governance tokens)");
  250. let vote = SubCommand::with_name("vote").about("Vote on a given proposal").args(&vec![
  251. bulla.clone(),
  252. vote,
  253. vote_weight,
  254. ]);
  255. let early = Arg::with_name("early").long("early").help("Execute the proposal early");
  256. let exec =
  257. SubCommand::with_name("exec").about("Execute a DAO proposal").args(&vec![bulla, early]);
  258. let spend_hook_cmd = SubCommand::with_name("spend-hook")
  259. .about("Print the DAO contract base58-encoded spend hook");
  260. let dao = SubCommand::with_name("dao").about("DAO functionalities").subcommands(vec![
  261. create,
  262. view,
  263. import,
  264. update_keys,
  265. list,
  266. balance,
  267. mint,
  268. propose_transfer,
  269. propose_generic,
  270. proposals,
  271. proposal,
  272. proposal_import,
  273. vote,
  274. exec,
  275. spend_hook_cmd,
  276. ]);
  277. // AttachFee
  278. let attach_fee = SubCommand::with_name("attach-fee")
  279. .about("Attach the fee call to a transaction given from stdin");
  280. // Inspect
  281. let inspect = SubCommand::with_name("inspect").about("Inspect a transaction from stdin");
  282. // Broadcast
  283. let broadcast =
  284. SubCommand::with_name("broadcast").about("Read a transaction from stdin and broadcast it");
  285. // Scan
  286. let reset = Arg::with_name("reset")
  287. .long("reset")
  288. .help("Reset wallet state to provided block height and start scanning");
  289. let scan = SubCommand::with_name("scan")
  290. .about("Scan the blockchain and parse relevant transactions")
  291. .args(&vec![reset]);
  292. // Explorer
  293. let tx_hash = Arg::with_name("tx-hash").help("Transaction hash");
  294. let encode = Arg::with_name("encode").long("encode").help("Encode transaction to base58");
  295. let fetch_tx = SubCommand::with_name("fetch-tx")
  296. .about("Fetch a blockchain transaction by hash")
  297. .args(&vec![tx_hash, encode]);
  298. let simulate_tx =
  299. SubCommand::with_name("simulate-tx").about("Read a transaction from stdin and simulate it");
  300. let tx_hash = Arg::with_name("tx-hash").help("Fetch specific history record (optional)");
  301. let encode = Arg::with_name("encode")
  302. .long("encode")
  303. .help("Encode specific history record transaction to base58");
  304. let txs_history = SubCommand::with_name("txs-history")
  305. .about("Fetch broadcasted transactions history")
  306. .args(&vec![tx_hash, encode]);
  307. let clear_reverted =
  308. SubCommand::with_name("clear-reverted").about("Remove reverted transactions from history");
  309. let height = Arg::with_name("height").help("Fetch specific height record (optional)");
  310. let scanned_blocks = SubCommand::with_name("scanned-blocks")
  311. .about("Fetch scanned blocks records")
  312. .args(&vec![height]);
  313. let explorer = SubCommand::with_name("explorer")
  314. .about("Explorer related subcommands")
  315. .subcommands(vec![fetch_tx, simulate_tx, txs_history, clear_reverted, scanned_blocks]);
  316. // Alias
  317. let alias = Arg::with_name("alias").help("Token alias");
  318. let token = Arg::with_name("token").help("Token to create alias for");
  319. let add = SubCommand::with_name("add").about("Create a Token alias").args(&vec![alias, token]);
  320. let alias = Arg::with_name("alias")
  321. .short("a")
  322. .long("alias")
  323. .takes_value(true)
  324. .help("Token alias to search for");
  325. let token = Arg::with_name("token")
  326. .short("t")
  327. .long("token")
  328. .takes_value(true)
  329. .help("Token to search alias for");
  330. let show = SubCommand::with_name("show")
  331. .about(
  332. "Print alias info of optional arguments. \
  333. If no argument is provided, list all the aliases in the wallet.",
  334. )
  335. .args(&vec![alias, token]);
  336. let alias = Arg::with_name("alias").help("Token alias to remove");
  337. let remove = SubCommand::with_name("remove").about("Remove a Token alias").arg(alias);
  338. let alias = SubCommand::with_name("alias")
  339. .about("Manage Token aliases")
  340. .subcommands(vec![add, show, remove]);
  341. // Token
  342. let secret_key = Arg::with_name("secret-key").help("Mint authority secret key");
  343. let token_blind = Arg::with_name("token-blind").help("Mint authority token blind");
  344. let import = SubCommand::with_name("import")
  345. .about("Import a mint authority")
  346. .args(&vec![secret_key, token_blind]);
  347. let generate_mint =
  348. SubCommand::with_name("generate-mint").about("Generate a new mint authority");
  349. let list =
  350. SubCommand::with_name("list").about("List token IDs with available mint authorities");
  351. let token = Arg::with_name("token").help("Token ID to mint");
  352. let amount = Arg::with_name("amount").help("Amount to mint");
  353. let recipient = Arg::with_name("recipient").help("Recipient of the minted tokens");
  354. let mint = SubCommand::with_name("mint")
  355. .about("Mint tokens")
  356. .args(&vec![token, amount, recipient, spend_hook, user_data]);
  357. let token = Arg::with_name("token").help("Token ID to freeze");
  358. let freeze = SubCommand::with_name("freeze").about("Freeze a token mint").arg(token);
  359. let token = SubCommand::with_name("token").about("Token functionalities").subcommands(vec![
  360. import,
  361. generate_mint,
  362. list,
  363. mint,
  364. freeze,
  365. ]);
  366. // Contract
  367. let generate_deploy =
  368. SubCommand::with_name("generate-deploy").about("Generate a new deploy authority");
  369. let list = SubCommand::with_name("list").about("List deploy authorities in the wallet");
  370. let deploy_auth = Arg::with_name("deploy-auth").help("Contract ID (deploy authority)");
  371. let wasm_path = Arg::with_name("wasm-path").help("Path to contract wasm bincode");
  372. let deploy_ix = Arg::with_name("deploy-ix").help("Path to serialized deploy instruction");
  373. let deploy = SubCommand::with_name("deploy").about("Deploy a smart contract").args(&vec![
  374. deploy_auth.clone(),
  375. wasm_path,
  376. deploy_ix,
  377. ]);
  378. let lock =
  379. SubCommand::with_name("lock").about("Lock a smart contract").args(&vec![deploy_auth]);
  380. let contract = SubCommand::with_name("contract")
  381. .about("Contract functionalities")
  382. .subcommands(vec![generate_deploy, list, deploy, lock]);
  383. // Main arguments
  384. let config = Arg::with_name("config")
  385. .short("c")
  386. .long("config")
  387. .takes_value(true)
  388. .help("Configuration file to use");
  389. let network = Arg::with_name("network")
  390. .long("network")
  391. .takes_value(true)
  392. .help("Blockchain network to use");
  393. let command = vec![
  394. interactive,
  395. kaching,
  396. ping,
  397. completions,
  398. wallet,
  399. spend,
  400. unspend,
  401. transfer,
  402. otc,
  403. attach_fee,
  404. inspect,
  405. broadcast,
  406. dao,
  407. scan,
  408. explorer,
  409. alias,
  410. token,
  411. contract,
  412. ];
  413. let fun = Arg::with_name("fun")
  414. .short("f")
  415. .long("fun")
  416. .help("Flag indicating whether you want some fun in your life");
  417. let log = Arg::with_name("log")
  418. .short("l")
  419. .long("log")
  420. .takes_value(true)
  421. .help("Set log file to ouput into");
  422. let verbose = Arg::with_name("verbose")
  423. .short("v")
  424. .multiple(true)
  425. .help("Increase verbosity (-vvv supported)");
  426. let mut app = App::new("drk")
  427. .about(cli_desc!())
  428. .args(&vec![config, network, fun, log, verbose])
  429. .subcommands(command);
  430. let shell = match Shell::from_str(shell) {
  431. Ok(s) => s,
  432. Err(e) => return Err(Error::Custom(e)),
  433. };
  434. let mut buf = vec![];
  435. app.gen_completions_to("./drk", shell, &mut buf);
  436. Ok(String::from_utf8(buf)?)
  437. }
  438. /// Auxiliary function to print provided string buffer.
  439. pub fn print_output(buf: &[String]) {
  440. for line in buf {
  441. println!("{line}");
  442. }
  443. }
  444. /// Auxiliary function to print or insert provided messages to given
  445. /// buffer reference. If a channel sender is provided, the messages
  446. /// are send to that instead.
  447. pub async fn append_or_print(
  448. buf: &mut Vec<String>,
  449. sender: Option<&Sender<Vec<String>>>,
  450. print: &bool,
  451. messages: Vec<String>,
  452. ) {
  453. // Send the messages to the channel, if provided
  454. if let Some(sender) = sender {
  455. if let Err(e) = sender.send(messages).await {
  456. let err_msg = format!("[append_or_print] Sending messages to channel failed: {e}");
  457. if *print {
  458. println!("{err_msg}");
  459. } else {
  460. buf.push(err_msg);
  461. }
  462. }
  463. return
  464. }
  465. // Print the messages
  466. if *print {
  467. for msg in messages {
  468. println!("{msg}");
  469. }
  470. return
  471. }
  472. // Insert the messages in the buffer
  473. for msg in messages {
  474. buf.push(msg);
  475. }
  476. }