main.rs 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915
  1. /* This file is part of DarkFi (https://dark.fi)
  2. *
  3. * Copyright (C) 2020-2023 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, Read},
  20. process::exit,
  21. str::FromStr,
  22. time::Instant,
  23. };
  24. use anyhow::{anyhow, Context, Result};
  25. use clap::{Parser, Subcommand};
  26. use darkfi::{tx::Transaction, util::parse::decode_base10, zk::halo2::Field};
  27. use darkfi_money_contract::client::Coin;
  28. use darkfi_sdk::{
  29. crypto::{PublicKey, SecretKey, TokenId},
  30. pasta::{group::ff::PrimeField, pallas},
  31. };
  32. use darkfi_serial::{deserialize, serialize};
  33. use prettytable::{format, row, Table};
  34. use rand::rngs::OsRng;
  35. use serde_json::json;
  36. use simplelog::{ColorChoice, TermLogger, TerminalMode};
  37. use url::Url;
  38. use darkfi::{
  39. cli_desc,
  40. rpc::{client::RpcClient, jsonrpc::JsonRequest},
  41. util::{
  42. cli::{get_log_config, get_log_level},
  43. parse::encode_base10,
  44. },
  45. };
  46. /// Airdrop methods
  47. mod rpc_airdrop;
  48. /// Payment methods
  49. mod rpc_transfer;
  50. /// Swap methods
  51. mod rpc_swap;
  52. use rpc_swap::PartialSwapData;
  53. /// DAO methods
  54. mod rpc_dao;
  55. /// Blockchain methods
  56. mod rpc_blockchain;
  57. /// CLI utility functions
  58. mod cli_util;
  59. use cli_util::{parse_token_pair, parse_value_pair};
  60. /// Wallet functionality related to DAO
  61. mod wallet_dao;
  62. use wallet_dao::DaoParams;
  63. /// Wallet functionality related to Money
  64. mod wallet_money;
  65. #[derive(Parser)]
  66. #[command(about = cli_desc!())]
  67. struct Args {
  68. #[arg(short, action = clap::ArgAction::Count)]
  69. /// Increase verbosity (-vvv supported)
  70. verbose: u8,
  71. #[arg(short, long, default_value = "tcp://127.0.0.1:8340")]
  72. /// darkfid JSON-RPC endpoint
  73. endpoint: Url,
  74. #[command(subcommand)]
  75. command: Subcmd,
  76. }
  77. #[derive(Subcommand)]
  78. enum Subcmd {
  79. /// Send a ping request to the darkfid RPC endpoint
  80. Ping,
  81. /// Wallet operations
  82. Wallet {
  83. #[arg(long)]
  84. /// Initialize wallet with data for Money Contract (run this first)
  85. initialize: bool,
  86. #[arg(long)]
  87. /// Generate a new keypair in the wallet
  88. keygen: bool,
  89. #[arg(long)]
  90. /// Query the wallet for known balances
  91. balance: bool,
  92. #[arg(long)]
  93. /// Get the default address in the wallet
  94. address: bool,
  95. #[arg(long)]
  96. /// Print all the secret keys from the wallet
  97. secrets: bool,
  98. #[arg(long)]
  99. /// Import secret keys from stdin into the wallet, separated by newlines
  100. import_secrets: bool,
  101. #[arg(long)]
  102. /// Print the Merkle tree in the wallet
  103. tree: bool,
  104. #[arg(long)]
  105. /// Print all the coins in the wallet
  106. coins: bool,
  107. },
  108. /// Unspend a coin
  109. Unspend {
  110. /// base58-encoded coin to mark as unspent
  111. coin: String,
  112. },
  113. /// Airdrop some tokens
  114. Airdrop {
  115. /// Faucet JSON-RPC endpoint
  116. #[arg(short, long, default_value = "tls://faucetd.testnet.dark.fi:18340")]
  117. faucet_endpoint: Url,
  118. /// Amount to request from the faucet
  119. amount: String,
  120. /// Token ID to request from the faucet
  121. token: String,
  122. /// Optional address to send tokens to (defaults to main address in wallet)
  123. address: Option<String>,
  124. },
  125. /// Create a payment transaction
  126. Transfer {
  127. /// Amount to send
  128. amount: String,
  129. /// Token ID to send
  130. token: String,
  131. /// Recipient address
  132. recipient: String,
  133. /// Mark if this is being sent to a DAO
  134. dao: bool,
  135. /// DAO bulla, if the tokens are being sent to a DAO
  136. dao_bulla: Option<String>,
  137. },
  138. /// OTC atomic swap
  139. #[command(subcommand, about = cli_desc!())]
  140. Otc(OtcSubcmd),
  141. /// Inspect a transaction from stdin
  142. Inspect,
  143. /// Read a transaction from stdin and broadcast it
  144. Broadcast,
  145. /// Subscribe to incoming blocks from darkfid
  146. ///
  147. /// This subscription will listen for incoming blocks from darkfid and look
  148. /// through their transactions to see if there's any that interest us.
  149. /// With `drk` we look at transactions calling the money contract so we can
  150. /// find coins sent to us and fill our wallet with the necessary metadata.
  151. Subscribe,
  152. /// DAO functionalities
  153. #[command(subcommand, about = cli_desc!())]
  154. Dao(DaoSubcmd),
  155. /// Scan the blockchain and parse relevant transactions
  156. Scan {
  157. #[arg(long)]
  158. /// Reset Merkle tree and start scanning from first slot
  159. reset: bool,
  160. #[arg(long)]
  161. /// List all available checkpoints
  162. list: bool,
  163. #[arg(short, long)]
  164. /// Reset Merkle tree to checkpoint index and start scanning
  165. checkpoint: Option<u64>,
  166. },
  167. }
  168. #[derive(Subcommand)]
  169. enum OtcSubcmd {
  170. /// Initialize the first half of the atomic swap
  171. Init {
  172. /// Value pair to send:recv (11.55:99.42)
  173. #[clap(short, long)]
  174. value_pair: String,
  175. /// Token pair to send:recv (f00:b4r)
  176. #[clap(short, long)]
  177. token_pair: String,
  178. },
  179. /// Build entire swap tx given the first half from stdin
  180. Join,
  181. /// Inspect a swap half or the full swap tx from stdin
  182. Inspect,
  183. /// Sign a transaction given from stdin as the first-half
  184. Sign,
  185. }
  186. #[derive(Subcommand)]
  187. enum DaoSubcmd {
  188. /// Create DAO parameters
  189. Create {
  190. /// The minimum amount of governance tokens needed to open a proposal for this DAO
  191. proposer_limit: u64,
  192. /// Minimal threshold of participating total tokens needed for a proposal to pass
  193. quorum: u64,
  194. /// The ratio of winning votes/total votes needed for a proposal to pass (2 decimals),
  195. approval_ratio: f64,
  196. /// DAO's governance token ID
  197. gov_token_id: String,
  198. },
  199. /// View DAO data from stdin
  200. View,
  201. /// Import DAO data from stdin
  202. Import {
  203. /// Named identifier for the DAO
  204. dao_name: String,
  205. },
  206. /// List imported DAOs (or info about a specific one)
  207. List {
  208. /// Numeric identifier for the DAO (optional)
  209. dao_id: Option<u64>,
  210. },
  211. /// Mint an imported DAO on-chain
  212. Mint {
  213. /// Numeric identifier for the DAO
  214. dao_id: u64,
  215. },
  216. /// Create a proposal for a DAO
  217. Propose {
  218. /// Numeric identifier for the DAO
  219. dao_id: u64,
  220. /// Pubkey to send tokens to with proposal success
  221. recipient: String,
  222. /// Amount to send from DAO with proposal success
  223. amount: String,
  224. /// Token ID to send from DAO with proposal success
  225. token_id: String,
  226. },
  227. /// List DAO proposals
  228. Proposals {
  229. /// Numeric identifier for the DAO
  230. dao_id: u64,
  231. },
  232. /// View a DAO proposal data
  233. Proposal {
  234. /// Numeric identifier for the DAO
  235. dao_id: u64,
  236. /// Numeric identifier for the proposal
  237. proposal_id: u64,
  238. },
  239. /// Vote on a given proposal
  240. Vote {
  241. /// Numeric identifier for the DAO
  242. dao_id: u64,
  243. /// Numeric identifier for the proposal
  244. proposal: u64,
  245. /// Vote (0 for NO, 1 for YES)
  246. vote: u8,
  247. /// Vote weight (amount of governance tokens)
  248. vote_weight: String,
  249. },
  250. /// Execute a DAO proposal
  251. Exec {
  252. /// Numeric identifier for the DAO
  253. dao_id: u64,
  254. /// Proposal identifier
  255. proposal: String,
  256. },
  257. }
  258. pub struct Drk {
  259. pub rpc_client: RpcClient,
  260. }
  261. impl Drk {
  262. async fn ping(&self) -> Result<()> {
  263. let latency = Instant::now();
  264. let req = JsonRequest::new("ping", json!([]));
  265. let rep = self.rpc_client.oneshot_request(req).await?;
  266. let latency = latency.elapsed();
  267. println!("Got reply: {}", rep);
  268. println!("Latency: {:?}", latency);
  269. Ok(())
  270. }
  271. }
  272. #[async_std::main]
  273. async fn main() -> Result<()> {
  274. let args = Args::parse();
  275. if args.verbose > 0 {
  276. let log_level = get_log_level(args.verbose.into());
  277. let log_config = get_log_config();
  278. TermLogger::init(log_level, log_config, TerminalMode::Mixed, ColorChoice::Auto)?;
  279. }
  280. match args.command {
  281. Subcmd::Ping => {
  282. let rpc_client = RpcClient::new(args.endpoint)
  283. .await
  284. .with_context(|| "Could not connect to darkfid RPC endpoint")?;
  285. let drk = Drk { rpc_client };
  286. drk.ping().await.with_context(|| "Failed to ping darkfid RPC endpoint")?;
  287. Ok(())
  288. }
  289. Subcmd::Wallet {
  290. initialize,
  291. keygen,
  292. balance,
  293. address,
  294. secrets,
  295. import_secrets,
  296. tree,
  297. coins,
  298. } => {
  299. if !initialize &&
  300. !keygen &&
  301. !balance &&
  302. !address &&
  303. !secrets &&
  304. !tree &&
  305. !coins &&
  306. !import_secrets
  307. {
  308. eprintln!("Error: You must use at least one flag for this subcommand");
  309. eprintln!("Run with \"wallet -h\" to see the subcommand usage.");
  310. exit(2);
  311. }
  312. let rpc_client = RpcClient::new(args.endpoint)
  313. .await
  314. .with_context(|| "Could not connect to darkfid RPC endpoint")?;
  315. let drk = Drk { rpc_client };
  316. if initialize {
  317. drk.initialize_money().await?;
  318. drk.initialize_dao().await?;
  319. return Ok(())
  320. }
  321. if keygen {
  322. drk.money_keygen().await.with_context(|| "Failed to generate keypair")?;
  323. return Ok(())
  324. }
  325. if balance {
  326. let balmap =
  327. drk.money_balance().await.with_context(|| "Failed to fetch wallet balance")?;
  328. // Create a prettytable with the new data:
  329. let mut table = Table::new();
  330. table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
  331. table.set_titles(row!["Token ID", "Balance"]);
  332. for (token_id, balance) in balmap.iter() {
  333. // FIXME: Don't hardcode to 8 decimals
  334. table.add_row(row![token_id, encode_base10(*balance, 8)]);
  335. }
  336. if table.is_empty() {
  337. println!("No unspent balances found");
  338. } else {
  339. println!("{}", table);
  340. }
  341. return Ok(())
  342. }
  343. if address {
  344. let address = drk
  345. .wallet_address(0)
  346. .await
  347. .with_context(|| "Failed to fetch default address")?;
  348. println!("{}", address);
  349. return Ok(())
  350. }
  351. if secrets {
  352. let v = drk
  353. .get_money_secrets()
  354. .await
  355. .with_context(|| "Failed to fetch wallet secrets")?;
  356. drk.rpc_client.close().await?;
  357. for i in v {
  358. println!("{}", i);
  359. }
  360. return Ok(())
  361. }
  362. if import_secrets {
  363. let mut secrets = vec![];
  364. let lines = stdin().lines();
  365. for (i, line) in lines.enumerate() {
  366. if let Ok(line) = line {
  367. let bytes = bs58::decode(&line.trim()).into_vec()?;
  368. let Ok(secret) = deserialize(&bytes) else {
  369. eprintln!("Warning: Failed to deserialize secret on line {}", i);
  370. continue
  371. };
  372. secrets.push(secret);
  373. }
  374. }
  375. let pubkeys = drk
  376. .import_money_secrets(secrets)
  377. .await
  378. .with_context(|| "Failed to import secret keys into wallet")?;
  379. drk.rpc_client.close().await?;
  380. for key in pubkeys {
  381. println!("{}", key);
  382. }
  383. return Ok(())
  384. }
  385. if tree {
  386. let v =
  387. drk.get_money_tree().await.with_context(|| "Failed to fetch Merkle tree")?;
  388. drk.rpc_client.close().await?;
  389. println!("{:#?}", v);
  390. return Ok(())
  391. }
  392. if coins {
  393. let coins = drk
  394. .get_coins(true)
  395. .await
  396. .with_context(|| "Failed to fetch coins from wallet")?;
  397. drk.rpc_client.close().await?;
  398. if coins.is_empty() {
  399. return Ok(())
  400. }
  401. let mut table = Table::new();
  402. table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
  403. table.set_titles(row!["Coin", "Spent", "Token ID", "Value"]);
  404. for coin in coins {
  405. table.add_row(row![
  406. format!("{:?}", coin.0.coin.inner()),
  407. coin.1,
  408. coin.0.note.token_id,
  409. format!("{} ({})", coin.0.note.value, encode_base10(coin.0.note.value, 8))
  410. ]);
  411. }
  412. println!("{}", table);
  413. return Ok(())
  414. }
  415. unreachable!()
  416. }
  417. Subcmd::Unspend { coin } => {
  418. let bytes: [u8; 32] = bs58::decode(&coin).into_vec()?.try_into().unwrap();
  419. let elem: pallas::Base = match pallas::Base::from_repr(bytes).into() {
  420. Some(v) => v,
  421. None => return Err(anyhow!("Invalid coin")),
  422. };
  423. let coin = Coin::from(elem);
  424. let rpc_client = RpcClient::new(args.endpoint)
  425. .await
  426. .with_context(|| "Could not connect to darkfid RPC endpoint")?;
  427. let drk = Drk { rpc_client };
  428. drk.unspend_coin(&coin).await.with_context(|| "Failed to mark coin as unspent")?;
  429. Ok(())
  430. }
  431. Subcmd::Airdrop { faucet_endpoint, amount, token, address } => {
  432. let amount = f64::from_str(&amount).with_context(|| "Invalid amount")?;
  433. let token_id = TokenId::try_from(token.as_str()).with_context(|| "Invalid Token ID")?;
  434. let rpc_client = RpcClient::new(args.endpoint)
  435. .await
  436. .with_context(|| "Could not connect to darkfid RPC endpoint")?;
  437. let drk = Drk { rpc_client };
  438. let address = match address {
  439. Some(v) => PublicKey::from_str(v.as_str()).with_context(|| "Invalid address")?,
  440. None => drk.wallet_address(0).await.with_context(|| {
  441. "Failed to fetch default address, perhaps the wallet was not initialized?"
  442. })?,
  443. };
  444. let txid = drk
  445. .request_airdrop(faucet_endpoint, amount, token_id, address)
  446. .await
  447. .with_context(|| "Failed to request airdrop")?;
  448. println!("Transaction ID: {}", txid);
  449. Ok(())
  450. }
  451. Subcmd::Transfer { amount, token, recipient, dao, dao_bulla } => {
  452. let _ = f64::from_str(&amount).with_context(|| "Invalid amount")?;
  453. let token_id = TokenId::try_from(token.as_str()).with_context(|| "Invalid Token ID")?;
  454. let rcpt = PublicKey::from_str(&recipient).with_context(|| "Invalid recipient")?;
  455. let rpc_client = RpcClient::new(args.endpoint)
  456. .await
  457. .with_context(|| "Could not connect to darkfid RPC endpoint")?;
  458. let drk = Drk { rpc_client };
  459. let tx = drk
  460. .transfer(&amount, token_id, rcpt, dao, dao_bulla)
  461. .await
  462. .with_context(|| "Failed to create payment transaction")?;
  463. println!("{}", bs58::encode(&serialize(&tx)).into_string());
  464. Ok(())
  465. }
  466. Subcmd::Otc(cmd) => {
  467. let rpc_client = RpcClient::new(args.endpoint)
  468. .await
  469. .with_context(|| "Could not connect to darkfid RPC endpoint")?;
  470. let drk = Drk { rpc_client };
  471. match cmd {
  472. OtcSubcmd::Init { value_pair, token_pair } => {
  473. let (vp_send, vp_recv) = parse_value_pair(&value_pair)?;
  474. let (tp_send, tp_recv) = parse_token_pair(&token_pair)?;
  475. let half = drk
  476. .init_swap(vp_send, tp_send, vp_recv, tp_recv)
  477. .await
  478. .with_context(|| "Failed to create swap transaction half")?;
  479. println!("{}", bs58::encode(&serialize(&half)).into_string());
  480. Ok(())
  481. }
  482. OtcSubcmd::Join => {
  483. let mut buf = String::new();
  484. stdin().read_to_string(&mut buf)?;
  485. let bytes = bs58::decode(&buf.trim()).into_vec()?;
  486. let partial: PartialSwapData = deserialize(&bytes)?;
  487. let tx = drk
  488. .join_swap(partial)
  489. .await
  490. .with_context(|| "Failed to create a join swap transaction")?;
  491. println!("{}", bs58::encode(&serialize(&tx)).into_string());
  492. Ok(())
  493. }
  494. OtcSubcmd::Inspect => {
  495. let mut buf = String::new();
  496. stdin().read_to_string(&mut buf)?;
  497. let bytes = bs58::decode(&buf.trim()).into_vec()?;
  498. drk.inspect_swap(bytes).await.with_context(|| "Failed to inspect swap")?;
  499. Ok(())
  500. }
  501. OtcSubcmd::Sign => {
  502. let mut buf = String::new();
  503. stdin().read_to_string(&mut buf)?;
  504. let bytes = bs58::decode(&buf.trim()).into_vec()?;
  505. let mut tx: Transaction = deserialize(&bytes)?;
  506. drk.sign_swap(&mut tx)
  507. .await
  508. .with_context(|| "Failed to sign joined swap transaction")?;
  509. println!("{}", bs58::encode(&serialize(&tx)).into_string());
  510. Ok(())
  511. }
  512. }
  513. }
  514. Subcmd::Inspect => {
  515. let mut buf = String::new();
  516. stdin().read_to_string(&mut buf)?;
  517. let bytes = bs58::decode(&buf.trim()).into_vec()?;
  518. let tx: Transaction = deserialize(&bytes)?;
  519. println!("{:#?}", tx);
  520. Ok(())
  521. }
  522. Subcmd::Broadcast => {
  523. eprintln!("Reading transaction from stdin...");
  524. let mut buf = String::new();
  525. stdin().read_to_string(&mut buf)?;
  526. let bytes = bs58::decode(&buf.trim()).into_vec()?;
  527. let tx = deserialize(&bytes)?;
  528. let rpc_client = RpcClient::new(args.endpoint)
  529. .await
  530. .with_context(|| "Could not connect to darkfid RPC endpoint")?;
  531. let drk = Drk { rpc_client };
  532. let txid =
  533. drk.broadcast_tx(&tx).await.with_context(|| "Failed to broadcast transaction")?;
  534. eprintln!("Transaction ID: {}", txid);
  535. Ok(())
  536. }
  537. Subcmd::Subscribe => {
  538. let rpc_client = RpcClient::new(args.endpoint.clone())
  539. .await
  540. .with_context(|| "Could not connect to darkfid RPC endpoint")?;
  541. let drk = Drk { rpc_client };
  542. drk.subscribe_blocks(args.endpoint)
  543. .await
  544. .with_context(|| "Block subscription failed")?;
  545. Ok(())
  546. }
  547. Subcmd::Scan { reset, list, checkpoint } => {
  548. let rpc_client = RpcClient::new(args.endpoint)
  549. .await
  550. .with_context(|| "Could not connect to darkfid RPC endpoint")?;
  551. let drk = Drk { rpc_client };
  552. if reset {
  553. eprintln!("Reset requested.");
  554. drk.scan_blocks(true).await.with_context(|| "Failed during scanning")?;
  555. return Ok(())
  556. }
  557. if list {
  558. eprintln!("List requested.");
  559. // TODO: implement
  560. return Ok(())
  561. }
  562. if let Some(c) = checkpoint {
  563. eprintln!("Checkpoint requested: {}", c);
  564. // TODO: implement
  565. return Ok(())
  566. }
  567. drk.scan_blocks(false).await.with_context(|| "Failed during scanning")?;
  568. eprintln!("Finished scanning blockchain");
  569. Ok(())
  570. }
  571. Subcmd::Dao(cmd) => match cmd {
  572. DaoSubcmd::Create { proposer_limit, quorum, approval_ratio, gov_token_id } => {
  573. if approval_ratio > 1.0 {
  574. eprintln!("Error: Approval ratio cannot be >1.0");
  575. exit(1);
  576. }
  577. let approval_ratio_quot = 100_u64;
  578. let approval_ratio_base = (approval_ratio * approval_ratio_quot as f64) as u64;
  579. let gov_token_id =
  580. TokenId::try_from(gov_token_id.as_str()).with_context(|| "Invalid Token ID")?;
  581. let secret_key = SecretKey::random(&mut OsRng);
  582. let bulla_blind = pallas::Base::random(&mut OsRng);
  583. let dao_params = DaoParams {
  584. proposer_limit,
  585. quorum,
  586. approval_ratio_base,
  587. approval_ratio_quot,
  588. gov_token_id,
  589. secret_key,
  590. bulla_blind,
  591. };
  592. let encoded = bs58::encode(&serialize(&dao_params)).into_string();
  593. println!("{}", encoded);
  594. Ok(())
  595. }
  596. DaoSubcmd::View => {
  597. let mut buf = String::new();
  598. stdin().read_to_string(&mut buf)?;
  599. let bytes = bs58::decode(&buf.trim()).into_vec()?;
  600. let dao_params: DaoParams = deserialize(&bytes)?;
  601. println!("DAO Parameters:");
  602. println!("Proposer limit: {}", dao_params.proposer_limit);
  603. println!("Quorum: {}", dao_params.quorum);
  604. println!(
  605. "Approval ratio: {}",
  606. dao_params.approval_ratio_base as f64 / dao_params.approval_ratio_quot as f64
  607. );
  608. println!("Governance token ID: {}", dao_params.gov_token_id);
  609. println!("Secret key: {}", dao_params.secret_key);
  610. println!("Bulla blind: {:?}", dao_params.bulla_blind);
  611. Ok(())
  612. }
  613. DaoSubcmd::Import { dao_name } => {
  614. let mut buf = String::new();
  615. stdin().read_to_string(&mut buf)?;
  616. let bytes = bs58::decode(&buf.trim()).into_vec()?;
  617. let dao_params: DaoParams = deserialize(&bytes)?;
  618. let rpc_client = RpcClient::new(args.endpoint.clone())
  619. .await
  620. .with_context(|| "Could not connect to darkfid RPC endpoint")?;
  621. let drk = Drk { rpc_client };
  622. drk.import_dao(dao_name, dao_params)
  623. .await
  624. .with_context(|| "Failed to import DAO")?;
  625. Ok(())
  626. }
  627. DaoSubcmd::List { dao_id } => {
  628. let rpc_client = RpcClient::new(args.endpoint.clone())
  629. .await
  630. .with_context(|| "Could not connect to darkfid RPC endpoint")?;
  631. let drk = Drk { rpc_client };
  632. drk.dao_list(dao_id).await.with_context(|| "Failed to list DAO")?;
  633. Ok(())
  634. }
  635. DaoSubcmd::Mint { dao_id } => {
  636. let rpc_client = RpcClient::new(args.endpoint.clone())
  637. .await
  638. .with_context(|| "Could not connect to darkfid RPC endpoint")?;
  639. let drk = Drk { rpc_client };
  640. let tx = drk.dao_mint(dao_id).await.with_context(|| "Failed to mint DAO")?;
  641. println!("{}", bs58::encode(&serialize(&tx)).into_string());
  642. Ok(())
  643. }
  644. DaoSubcmd::Propose { dao_id, recipient, amount, token_id } => {
  645. let _ = f64::from_str(&amount).with_context(|| "Invalid amount")?;
  646. let amount = decode_base10(&amount, 8, true)?;
  647. let rcpt = PublicKey::from_str(&recipient).with_context(|| "Invalid recipient")?;
  648. let token_id =
  649. TokenId::try_from(token_id.as_str()).with_context(|| "Invalid Token ID")?;
  650. let rpc_client = RpcClient::new(args.endpoint.clone())
  651. .await
  652. .with_context(|| "Could not connect to darkfid RPC endpoint")?;
  653. let drk = Drk { rpc_client };
  654. let tx = drk
  655. .dao_propose(dao_id, rcpt, amount, token_id)
  656. .await
  657. .with_context(|| "Failed to create DAO proposal")?;
  658. println!("{}", bs58::encode(&serialize(&tx)).into_string());
  659. Ok(())
  660. }
  661. DaoSubcmd::Proposals { dao_id } => {
  662. let rpc_client = RpcClient::new(args.endpoint.clone())
  663. .await
  664. .with_context(|| "Could not connect to darkfid RPC endpoint")?;
  665. let drk = Drk { rpc_client };
  666. let proposals = drk.get_dao_proposals(dao_id).await?;
  667. for proposal in proposals {
  668. println!("[{}] {:?}", proposal.id, proposal.bulla());
  669. }
  670. Ok(())
  671. }
  672. DaoSubcmd::Proposal { dao_id, proposal_id } => {
  673. let rpc_client = RpcClient::new(args.endpoint.clone())
  674. .await
  675. .with_context(|| "Could not connect to darkfid RPC endpoint")?;
  676. let drk = Drk { rpc_client };
  677. let proposals = drk.get_dao_proposals(dao_id).await?;
  678. let Some(proposal) = proposals.iter().find(|x| x.id == proposal_id) else {
  679. eprintln!("No such DAO proposal found");
  680. exit(1);
  681. };
  682. println!("Proposal parameters:");
  683. println!("DAO Bulla: {:?}", proposal.dao_bulla);
  684. println!("Recipient: {}", proposal.recipient);
  685. println!(
  686. "Proposal amount {} ({})",
  687. encode_base10(proposal.amount, 8),
  688. proposal.amount
  689. );
  690. println!("Proposal serial: {:?}", proposal.serial);
  691. println!("Proposal token ID: {}", proposal.token_id);
  692. println!("Proposal bulla blind: {:?}", proposal.bulla_blind);
  693. println!("Proposal leaf position: {:?}", proposal.leaf_position);
  694. println!("Proposal tx hash: {:?}", proposal.tx_hash);
  695. println!("Proposal call index: {:?}", proposal.call_index);
  696. println!("Proposal vote ID: {:?}", proposal.vote_id);
  697. Ok(())
  698. }
  699. DaoSubcmd::Vote { dao_id, proposal, vote, vote_weight } => {
  700. let rpc_client = RpcClient::new(args.endpoint.clone())
  701. .await
  702. .with_context(|| "Could not connect to darkfid RPC endpoint")?;
  703. let drk = Drk { rpc_client };
  704. let _ = f64::from_str(&vote_weight).with_context(|| "Invalid vote weight")?;
  705. let weight = decode_base10(&vote_weight, 8, true)?;
  706. if vote > 1 {
  707. eprintln!("Vote can be either 0 (NO) or 1 (YES)");
  708. exit(1);
  709. }
  710. let vote = vote != 0;
  711. let tx = drk
  712. .dao_vote(dao_id, proposal, vote, weight)
  713. .await
  714. .with_context(|| "Failed to create DAO Vote transaction")?;
  715. println!("{}", bs58::encode(&serialize(&tx)).into_string());
  716. Ok(())
  717. }
  718. DaoSubcmd::Exec { dao_id, proposal } => todo!(),
  719. },
  720. }
  721. }