cli_util.rs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. /* This file is part of DarkFi (https://dark.fi)
  2. *
  3. * Copyright (C) 2020-2024 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. process::exit,
  21. str::FromStr,
  22. };
  23. use rodio::{source::Source, Decoder, OutputStream};
  24. use structopt_toml::clap::{App, Arg, Shell, SubCommand};
  25. use darkfi::{
  26. cli_desc,
  27. system::sleep,
  28. tx::Transaction,
  29. util::{encoding::base64, parse::decode_base10},
  30. Error, Result,
  31. };
  32. use darkfi_money_contract::model::TokenId;
  33. use darkfi_serial::deserialize_async;
  34. use crate::{money::BALANCE_BASE10_DECIMALS, Drk};
  35. /// Auxiliary function to parse a base64 encoded transaction from stdin.
  36. pub async fn parse_tx_from_stdin() -> Result<Transaction> {
  37. let mut buf = String::new();
  38. stdin().read_to_string(&mut buf)?;
  39. let Some(bytes) = base64::decode(buf.trim()) else {
  40. eprintln!("Failed to decode transaction");
  41. exit(2);
  42. };
  43. Ok(deserialize_async(&bytes).await?)
  44. }
  45. /// Auxiliary function to parse provided string into a values pair.
  46. pub fn parse_value_pair(s: &str) -> Result<(u64, u64)> {
  47. let v: Vec<&str> = s.split(':').collect();
  48. if v.len() != 2 {
  49. eprintln!("Invalid value pair. Use a pair such as 13.37:11.0");
  50. exit(2);
  51. }
  52. let val0 = decode_base10(v[0], BALANCE_BASE10_DECIMALS, true);
  53. let val1 = decode_base10(v[1], BALANCE_BASE10_DECIMALS, true);
  54. if val0.is_err() || val1.is_err() {
  55. eprintln!("Invalid value pair. Use a pair such as 13.37:11.0");
  56. exit(2);
  57. }
  58. Ok((val0.unwrap(), val1.unwrap()))
  59. }
  60. /// Auxiliary function to parse provided string into a tokens pair.
  61. pub async fn parse_token_pair(drk: &Drk, s: &str) -> Result<(TokenId, TokenId)> {
  62. let v: Vec<&str> = s.split(':').collect();
  63. if v.len() != 2 {
  64. eprintln!("Invalid token pair. Use a pair such as:");
  65. eprintln!("WCKD:MLDY");
  66. eprintln!("or");
  67. eprintln!("A7f1RKsCUUHrSXA7a9ogmwg8p3bs6F47ggsW826HD4yd:FCuoMii64H5Ee4eVWBjP18WTFS8iLUJmGi16Qti1xFQ2");
  68. exit(2);
  69. }
  70. let tok0 = drk.get_token(v[0].to_string()).await;
  71. let tok1 = drk.get_token(v[1].to_string()).await;
  72. if tok0.is_err() || tok1.is_err() {
  73. eprintln!("Invalid token pair. Use a pair such as:");
  74. eprintln!("WCKD:MLDY");
  75. eprintln!("or");
  76. eprintln!("A7f1RKsCUUHrSXA7a9ogmwg8p3bs6F47ggsW826HD4yd:FCuoMii64H5Ee4eVWBjP18WTFS8iLUJmGi16Qti1xFQ2");
  77. exit(2);
  78. }
  79. Ok((tok0.unwrap(), tok1.unwrap()))
  80. }
  81. /// Fun police go away
  82. pub async fn kaching() {
  83. const WALLET_MP3: &[u8] = include_bytes!("../wallet.mp3");
  84. let cursor = Cursor::new(WALLET_MP3);
  85. let Ok((_stream, stream_handle)) = OutputStream::try_default() else { return };
  86. let Ok(source) = Decoder::new(cursor) else { return };
  87. if stream_handle.play_raw(source.convert_samples()).is_err() {
  88. return
  89. }
  90. sleep(2).await;
  91. }
  92. /// Auxiliary function to generate provided shell completions.
  93. pub fn generate_completions(shell: &str) -> Result<()> {
  94. // Sub-commands
  95. // Kaching
  96. let kaching = SubCommand::with_name("kaching").about("Fun");
  97. // Ping
  98. let ping =
  99. SubCommand::with_name("ping").about("Send a ping request to the darkfid RPC endpoint");
  100. // Completions
  101. let shell_arg = Arg::with_name("shell").help("The Shell you want to generate script for");
  102. let completions = SubCommand::with_name("completions")
  103. .about("Generate a SHELL completion script and print to stdout")
  104. .arg(shell_arg);
  105. // Wallet
  106. let initialize =
  107. Arg::with_name("initialize").long("initialize").help("Initialize wallet database");
  108. let keygen =
  109. Arg::with_name("keygen").long("keygen").help("Generate a new keypair in the wallet");
  110. let balance =
  111. Arg::with_name("balance").long("balance").help("Query the wallet for known balances");
  112. let address =
  113. Arg::with_name("address").long("address").help("Get the default address in the wallet");
  114. let addresses =
  115. Arg::with_name("addresses").long("addresses").help("Print all the addresses in the wallet");
  116. let default_address = Arg::with_name("default-address")
  117. .long("default-address")
  118. .takes_value(true)
  119. .help("Set the default address in the wallet");
  120. let secrets =
  121. Arg::with_name("secrets").long("secrets").help("Print all the secret keys from the wallet");
  122. let import_secrets = Arg::with_name("import-secrets")
  123. .long("import-secrets")
  124. .help("Import secret keys from stdin into the wallet, separated by newlines");
  125. let tree = Arg::with_name("tree").long("tree").help("Print the Merkle tree in the wallet");
  126. let coins = Arg::with_name("coins").long("coins").help("Print all the coins in the wallet");
  127. let wallet = SubCommand::with_name("wallet").about("Wallet operations").args(&vec![
  128. initialize,
  129. keygen,
  130. balance,
  131. address,
  132. addresses,
  133. default_address,
  134. secrets,
  135. import_secrets,
  136. tree,
  137. coins,
  138. ]);
  139. // Spend
  140. let spend = SubCommand::with_name("spend")
  141. .about("Read a transaction from stdin and mark its input coins as spent");
  142. // Unspend
  143. let coin = Arg::with_name("coin").help("base58-encoded coin to mark as unspent");
  144. let unspend = SubCommand::with_name("unspend").about("Unspend a coin").arg(coin);
  145. // Transfer
  146. let amount = Arg::with_name("amount").help("Amount to send");
  147. let token = Arg::with_name("token").help("Token ID to send");
  148. let recipient = Arg::with_name("recipient").help("Recipient address");
  149. let spend_hook = Arg::with_name("spend-hook").help("Optional contract spend hook to use");
  150. let user_data = Arg::with_name("user-data").help("Optional user data to use");
  151. let transfer =
  152. SubCommand::with_name("transfer").about("Create a payment transaction").args(&vec![
  153. amount.clone(),
  154. token.clone(),
  155. recipient.clone(),
  156. spend_hook.clone(),
  157. user_data.clone(),
  158. ]);
  159. // Otc
  160. let value_pair = Arg::with_name("value-pair")
  161. .short("v")
  162. .long("value-pair")
  163. .takes_value(true)
  164. .help("Value pair to send:recv (11.55:99.42)");
  165. let token_pair = Arg::with_name("token-pair")
  166. .short("t")
  167. .long("token-pair")
  168. .takes_value(true)
  169. .help("Token pair to send:recv (f00:b4r)");
  170. let init = SubCommand::with_name("init")
  171. .about("Initialize the first half of the atomic swap")
  172. .args(&vec![value_pair, token_pair]);
  173. let join =
  174. SubCommand::with_name("join").about("Build entire swap tx given the first half from stdin");
  175. let inspect = SubCommand::with_name("inspect")
  176. .about("Inspect a swap half or the full swap tx from stdin");
  177. let sign = SubCommand::with_name("sign").about("Sign a swap transaction given from stdin");
  178. let otc = SubCommand::with_name("otc")
  179. .about("OTC atomic swap")
  180. .subcommands(vec![init, join, inspect, sign]);
  181. // AttachFee
  182. let attach_fee = SubCommand::with_name("attach-fee")
  183. .about("Attach the fee call to a transaction given from stdin");
  184. // Inspect
  185. let inspect = SubCommand::with_name("inspect").about("Inspect a transaction from stdin");
  186. // Broadcast
  187. let broadcast =
  188. SubCommand::with_name("broadcast").about("Read a transaction from stdin and broadcast it");
  189. // Subscribe
  190. let subscribe = SubCommand::with_name("subscribe").about(
  191. "This subscription will listen for incoming blocks from darkfid and look \
  192. through their transactions to see if there's any that interest us. \
  193. With `drk` we look at transactions calling the money contract so we can \
  194. find coins sent to us and fill our wallet with the necessary metadata.",
  195. );
  196. // DAO
  197. let proposer_limit = Arg::with_name("proposer-limit")
  198. .help("The minimum amount of governance tokens needed to open a proposal for this DAO");
  199. let quorum = Arg::with_name("quorum")
  200. .help("Minimal threshold of participating total tokens needed for a proposal to pass");
  201. let approval_ratio = Arg::with_name("approval-ratio")
  202. .help("The ratio of winning votes/total votes needed for a proposal to pass (2 decimals)");
  203. let gov_token_id = Arg::with_name("gov-token-id").help("DAO's governance token ID");
  204. let create = SubCommand::with_name("create").about("Create DAO parameters").args(&vec![
  205. proposer_limit,
  206. quorum,
  207. approval_ratio,
  208. gov_token_id,
  209. ]);
  210. let view = SubCommand::with_name("view").about("View DAO data from stdin");
  211. let name = Arg::with_name("name").help("Name identifier for the DAO");
  212. let import = SubCommand::with_name("import")
  213. .about("Import DAO data from stdin")
  214. .args(&vec![name.clone()]);
  215. let opt_name = Arg::with_name("dao-alias").help("Name identifier for the DAO (optional)");
  216. let list = SubCommand::with_name("list")
  217. .about("List imported DAOs (or info about a specific one)")
  218. .args(&vec![opt_name]);
  219. let balance = SubCommand::with_name("balance")
  220. .about("Show the balance of a DAO")
  221. .args(&vec![name.clone()]);
  222. let mint = SubCommand::with_name("mint")
  223. .about("Mint an imported DAO on-chain")
  224. .args(&vec![name.clone()]);
  225. let duration = Arg::with_name("duration").help("Duration of the proposal, in days");
  226. let propose_transfer = SubCommand::with_name("propose-transfer")
  227. .about("Create a transfer proposal for a DAO")
  228. .args(&vec![
  229. name.clone(),
  230. duration,
  231. amount,
  232. token,
  233. recipient,
  234. spend_hook.clone(),
  235. user_data.clone(),
  236. ]);
  237. let proposals =
  238. SubCommand::with_name("proposals").about("List DAO proposals").args(&vec![name]);
  239. let bulla = Arg::with_name("bulla").help("Bulla identifier for the proposal");
  240. let export = Arg::with_name("export").help("Encrypt the proposal and encode it to base64");
  241. let mint_proposal = Arg::with_name("mint-proposal").help("Create the proposal transaction");
  242. let proposal = SubCommand::with_name("proposal").about("View a DAO proposal data").args(&vec![
  243. bulla.clone(),
  244. export,
  245. mint_proposal,
  246. ]);
  247. let proposal_import = SubCommand::with_name("proposal-import")
  248. .about("Import a base64 encoded and encrypted proposal from stdin");
  249. let vote = Arg::with_name("vote").help("Vote (0 for NO, 1 for YES)");
  250. let vote_weight =
  251. Arg::with_name("vote-weight").help("Optional vote weight (amount of governance tokens)");
  252. let vote = SubCommand::with_name("vote").about("Vote on a given proposal").args(&vec![
  253. bulla.clone(),
  254. vote,
  255. vote_weight,
  256. ]);
  257. let exec = SubCommand::with_name("exec").about("Execute a DAO proposal").args(&vec![bulla]);
  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. list,
  265. balance,
  266. mint,
  267. propose_transfer,
  268. proposals,
  269. proposal,
  270. proposal_import,
  271. vote,
  272. exec,
  273. spend_hook_cmd,
  274. ]);
  275. // Scan
  276. let reset = Arg::with_name("reset")
  277. .long("reset")
  278. .help("Reset Merkle tree and start scanning from first block");
  279. let scan = SubCommand::with_name("scan")
  280. .about("Scan the blockchain and parse relevant transactions")
  281. .args(&vec![reset]);
  282. // Explorer
  283. let tx_hash = Arg::with_name("tx-hash").help("Transaction hash");
  284. let full = Arg::with_name("full").long("full").help("Print the full transaction information");
  285. let encode = Arg::with_name("encode").long("encode").help("Encode transaction to base58");
  286. let fetch_tx = SubCommand::with_name("fetch-tx")
  287. .about("Fetch a blockchain transaction by hash")
  288. .args(&vec![tx_hash, full, encode]);
  289. let simulate_tx =
  290. SubCommand::with_name("simulate-tx").about("Read a transaction from stdin and simulate it");
  291. let tx_hash = Arg::with_name("tx-hash").help("Transaction hash");
  292. let encode = Arg::with_name("encode")
  293. .long("encode")
  294. .help("Encode specific history record transaction to base58");
  295. let txs_history = SubCommand::with_name("txs-history")
  296. .about("Fetch broadcasted transactions history")
  297. .args(&vec![tx_hash, encode]);
  298. let explorer = SubCommand::with_name("explorer")
  299. .about("Explorer related subcommands")
  300. .subcommands(vec![fetch_tx, simulate_tx, txs_history]);
  301. // Alias
  302. let alias = Arg::with_name("alias").help("Token alias");
  303. let token = Arg::with_name("token").help("Token to create alias for");
  304. let add = SubCommand::with_name("add").about("Create a Token alias").args(&vec![alias, token]);
  305. let alias = Arg::with_name("alias")
  306. .short("a")
  307. .long("alias")
  308. .takes_value(true)
  309. .help("Token alias to search for");
  310. let token = Arg::with_name("token")
  311. .short("t")
  312. .long("token")
  313. .takes_value(true)
  314. .help("Token to search alias for");
  315. let show = SubCommand::with_name("show")
  316. .about(
  317. "Print alias info of optional arguments. \
  318. If no argument is provided, list all the aliases in the wallet.",
  319. )
  320. .args(&vec![alias, token]);
  321. let alias = Arg::with_name("alias").help("Token alias to remove");
  322. let remove = SubCommand::with_name("remove").about("Remove a Token alias").arg(alias);
  323. let alias = SubCommand::with_name("alias")
  324. .about("Manage Token aliases")
  325. .subcommands(vec![add, show, remove]);
  326. // Token
  327. let secret_key = Arg::with_name("secret-key").help("Mint authority secret key");
  328. let token_blind = Arg::with_name("token-blind").help("Mint authority token blind");
  329. let import = SubCommand::with_name("import")
  330. .about("Import a mint authority")
  331. .args(&vec![secret_key, token_blind]);
  332. let generate_mint =
  333. SubCommand::with_name("generate-mint").about("Generate a new mint authority");
  334. let list =
  335. SubCommand::with_name("list").about("List token IDs with available mint authorities");
  336. let token = Arg::with_name("token").help("Token ID to mint");
  337. let amount = Arg::with_name("amount").help("Amount to mint");
  338. let recipient = Arg::with_name("recipient").help("Recipient of the minted tokens");
  339. let mint = SubCommand::with_name("mint")
  340. .about("Mint tokens")
  341. .args(&vec![token, amount, recipient, spend_hook, user_data]);
  342. let token = Arg::with_name("token").help("Token ID to freeze");
  343. let freeze = SubCommand::with_name("freeze").about("Freeze a token mint").arg(token);
  344. let token = SubCommand::with_name("token").about("Token functionalities").subcommands(vec![
  345. import,
  346. generate_mint,
  347. list,
  348. mint,
  349. freeze,
  350. ]);
  351. // Main arguments
  352. let config = Arg::with_name("config")
  353. .short("c")
  354. .long("config")
  355. .takes_value(true)
  356. .help("Configuration file to use");
  357. let wallet_path = Arg::with_name("wallet_path")
  358. .long("wallet-path")
  359. .takes_value(true)
  360. .help("Path to wallet database");
  361. let wallet_pass = Arg::with_name("wallet_pass")
  362. .long("wallet-pass")
  363. .takes_value(true)
  364. .help("Password for the wallet database");
  365. let endpoint = Arg::with_name("endpoint")
  366. .short("e")
  367. .long("endpoint")
  368. .takes_value(true)
  369. .help("darkfid JSON-RPC endpoint");
  370. let command = vec![
  371. kaching,
  372. ping,
  373. completions,
  374. wallet,
  375. spend,
  376. unspend,
  377. transfer,
  378. otc,
  379. attach_fee,
  380. inspect,
  381. broadcast,
  382. subscribe,
  383. dao,
  384. scan,
  385. explorer,
  386. alias,
  387. token,
  388. ];
  389. let log = Arg::with_name("log")
  390. .short("l")
  391. .long("log")
  392. .takes_value(true)
  393. .help("Set log file to ouput into");
  394. let verbose = Arg::with_name("verbose")
  395. .short("v")
  396. .multiple(true)
  397. .help("Increase verbosity (-vvv supported)");
  398. let mut app = App::new("drk")
  399. .about(cli_desc!())
  400. .args(&vec![config, wallet_path, wallet_pass, endpoint, log, verbose])
  401. .subcommands(command);
  402. let shell = match Shell::from_str(shell) {
  403. Ok(s) => s,
  404. Err(e) => return Err(Error::Custom(e)),
  405. };
  406. app.gen_completions_to("./drk", shell, &mut std::io::stdout());
  407. Ok(())
  408. }