main.rs 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065
  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. #[clap(long)]
  135. dao: bool,
  136. /// DAO bulla, if the tokens are being sent to a DAO
  137. dao_bulla: Option<String>,
  138. },
  139. /// OTC atomic swap
  140. #[command(subcommand)]
  141. Otc(OtcSubcmd),
  142. /// Inspect a transaction from stdin
  143. Inspect,
  144. /// Read a transaction from stdin and broadcast it
  145. Broadcast,
  146. /// Subscribe to incoming blocks from darkfid
  147. ///
  148. /// This subscription will listen for incoming blocks from darkfid and look
  149. /// through their transactions to see if there's any that interest us.
  150. /// With `drk` we look at transactions calling the money contract so we can
  151. /// find coins sent to us and fill our wallet with the necessary metadata.
  152. Subscribe,
  153. /// DAO functionalities
  154. #[command(subcommand)]
  155. Dao(DaoSubcmd),
  156. /// Scan the blockchain and parse relevant transactions
  157. Scan {
  158. #[arg(long)]
  159. /// Reset Merkle tree and start scanning from first slot
  160. reset: bool,
  161. #[arg(long)]
  162. /// List all available checkpoints
  163. list: bool,
  164. #[arg(short, long)]
  165. /// Reset Merkle tree to checkpoint index and start scanning
  166. checkpoint: Option<u64>,
  167. },
  168. /// Explorer related subcommands
  169. #[command(subcommand)]
  170. Explorer(ExplorerSubcmd),
  171. /// Manage Token aliases
  172. #[command(subcommand)]
  173. Alias(AliasSubcmd),
  174. }
  175. #[derive(Subcommand)]
  176. enum OtcSubcmd {
  177. /// Initialize the first half of the atomic swap
  178. Init {
  179. /// Value pair to send:recv (11.55:99.42)
  180. #[clap(short, long)]
  181. value_pair: String,
  182. /// Token pair to send:recv (f00:b4r)
  183. #[clap(short, long)]
  184. token_pair: String,
  185. },
  186. /// Build entire swap tx given the first half from stdin
  187. Join,
  188. /// Inspect a swap half or the full swap tx from stdin
  189. Inspect,
  190. /// Sign a transaction given from stdin as the first-half
  191. Sign,
  192. }
  193. #[derive(Subcommand)]
  194. enum DaoSubcmd {
  195. /// Create DAO parameters
  196. Create {
  197. /// The minimum amount of governance tokens needed to open a proposal for this DAO
  198. proposer_limit: String,
  199. /// Minimal threshold of participating total tokens needed for a proposal to pass
  200. quorum: String,
  201. /// The ratio of winning votes/total votes needed for a proposal to pass (2 decimals),
  202. approval_ratio: f64,
  203. /// DAO's governance token ID
  204. gov_token_id: String,
  205. },
  206. /// View DAO data from stdin
  207. View,
  208. /// Import DAO data from stdin
  209. Import {
  210. /// Named identifier for the DAO
  211. dao_name: String,
  212. },
  213. /// List imported DAOs (or info about a specific one)
  214. List {
  215. /// Numeric identifier for the DAO (optional)
  216. dao_id: Option<u64>,
  217. },
  218. /// Show the balance of a DAO
  219. Balance {
  220. /// Numeric identifier for the DAO
  221. dao_id: u64,
  222. },
  223. /// Mint an imported DAO on-chain
  224. Mint {
  225. /// Numeric identifier for the DAO
  226. dao_id: u64,
  227. },
  228. /// Create a proposal for a DAO
  229. Propose {
  230. /// Numeric identifier for the DAO
  231. dao_id: u64,
  232. /// Pubkey to send tokens to with proposal success
  233. recipient: String,
  234. /// Amount to send from DAO with proposal success
  235. amount: String,
  236. /// Token ID to send from DAO with proposal success
  237. token_id: String,
  238. },
  239. /// List DAO proposals
  240. Proposals {
  241. /// Numeric identifier for the DAO
  242. dao_id: u64,
  243. },
  244. /// View a DAO proposal data
  245. Proposal {
  246. /// Numeric identifier for the DAO
  247. dao_id: u64,
  248. /// Numeric identifier for the proposal
  249. proposal_id: u64,
  250. },
  251. /// Vote on a given proposal
  252. Vote {
  253. /// Numeric identifier for the DAO
  254. dao_id: u64,
  255. /// Numeric identifier for the proposal
  256. proposal_id: u64,
  257. /// Vote (0 for NO, 1 for YES)
  258. vote: u8,
  259. /// Vote weight (amount of governance tokens)
  260. vote_weight: String,
  261. },
  262. /// Execute a DAO proposal
  263. Exec {
  264. /// Numeric identifier for the DAO
  265. dao_id: u64,
  266. /// Numeric identifier for the proposal
  267. proposal_id: u64,
  268. },
  269. }
  270. #[derive(Subcommand)]
  271. enum ExplorerSubcmd {
  272. /// Fetch a blockchain transaction by hash
  273. FetchTx {
  274. /// Transaction hash
  275. tx_hash: String,
  276. },
  277. /// Read a transaction from stdin and simulate it
  278. SimulateTx,
  279. }
  280. #[derive(Subcommand)]
  281. enum AliasSubcmd {
  282. /// Create a Token alias
  283. Add {
  284. /// Token alias
  285. alias: String,
  286. /// Token to create alias for
  287. token: String,
  288. },
  289. /// Print alias info of optional arguments.
  290. /// If no argument is provided, list all the aliases in the wallet.
  291. Show {
  292. /// Token alias to search for
  293. #[clap(short, long)]
  294. alias: Option<String>,
  295. /// Token to search alias for
  296. #[clap(short, long)]
  297. token: Option<String>,
  298. },
  299. /// Remove a Token alias
  300. Remove {
  301. /// Token alias to remove
  302. alias: String,
  303. },
  304. }
  305. pub struct Drk {
  306. pub rpc_client: RpcClient,
  307. }
  308. impl Drk {
  309. async fn new(endpoint: Url) -> Result<Self> {
  310. let rpc_client = RpcClient::new(endpoint).await?;
  311. Ok(Self { rpc_client })
  312. }
  313. async fn ping(&self) -> Result<()> {
  314. let latency = Instant::now();
  315. let req = JsonRequest::new("ping", json!([]));
  316. let rep = self.rpc_client.oneshot_request(req).await?;
  317. let latency = latency.elapsed();
  318. println!("Got reply: {}", rep);
  319. println!("Latency: {:?}", latency);
  320. Ok(())
  321. }
  322. }
  323. #[async_std::main]
  324. async fn main() -> Result<()> {
  325. let args = Args::parse();
  326. if args.verbose > 0 {
  327. let log_level = get_log_level(args.verbose.into());
  328. let log_config = get_log_config();
  329. TermLogger::init(log_level, log_config, TerminalMode::Mixed, ColorChoice::Auto)?;
  330. }
  331. match args.command {
  332. Subcmd::Ping => {
  333. let drk = Drk::new(args.endpoint).await?;
  334. drk.ping().await.with_context(|| "Failed to ping darkfid RPC endpoint")?;
  335. Ok(())
  336. }
  337. Subcmd::Wallet {
  338. initialize,
  339. keygen,
  340. balance,
  341. address,
  342. secrets,
  343. import_secrets,
  344. tree,
  345. coins,
  346. } => {
  347. if !initialize &&
  348. !keygen &&
  349. !balance &&
  350. !address &&
  351. !secrets &&
  352. !tree &&
  353. !coins &&
  354. !import_secrets
  355. {
  356. eprintln!("Error: You must use at least one flag for this subcommand");
  357. eprintln!("Run with \"wallet -h\" to see the subcommand usage.");
  358. exit(2);
  359. }
  360. let drk = Drk::new(args.endpoint).await?;
  361. if initialize {
  362. drk.initialize_money().await?;
  363. drk.initialize_dao().await?;
  364. return Ok(())
  365. }
  366. if keygen {
  367. drk.money_keygen().await.with_context(|| "Failed to generate keypair")?;
  368. return Ok(())
  369. }
  370. if balance {
  371. let balmap =
  372. drk.money_balance().await.with_context(|| "Failed to fetch wallet balance")?;
  373. let aliases_map = drk
  374. .get_aliases_mapped_by_token()
  375. .await
  376. .with_context(|| "Failed to fetch wallet aliases")?;
  377. // Create a prettytable with the new data:
  378. let mut table = Table::new();
  379. table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
  380. table.set_titles(row!["Token ID", "Aliases", "Balance"]);
  381. for (token_id, balance) in balmap.iter() {
  382. let aliases = match aliases_map.get(token_id) {
  383. Some(a) => a,
  384. None => "-",
  385. };
  386. // FIXME: Don't hardcode to 8 decimals
  387. table.add_row(row![token_id, aliases, encode_base10(*balance, 8)]);
  388. }
  389. if table.is_empty() {
  390. println!("No unspent balances found");
  391. } else {
  392. println!("{}", table);
  393. }
  394. return Ok(())
  395. }
  396. if address {
  397. let address = drk
  398. .wallet_address(1) // <-- TODO: Use is_default from the sql table
  399. .await
  400. .with_context(|| "Failed to fetch default address")?;
  401. println!("{}", address);
  402. return Ok(())
  403. }
  404. if secrets {
  405. let v = drk
  406. .get_money_secrets()
  407. .await
  408. .with_context(|| "Failed to fetch wallet secrets")?;
  409. drk.rpc_client.close().await?;
  410. for i in v {
  411. println!("{}", i);
  412. }
  413. return Ok(())
  414. }
  415. if import_secrets {
  416. let mut secrets = vec![];
  417. let lines = stdin().lines();
  418. for (i, line) in lines.enumerate() {
  419. if let Ok(line) = line {
  420. let bytes = bs58::decode(&line.trim()).into_vec()?;
  421. let Ok(secret) = deserialize(&bytes) else {
  422. eprintln!("Warning: Failed to deserialize secret on line {}", i);
  423. continue
  424. };
  425. secrets.push(secret);
  426. }
  427. }
  428. let pubkeys = drk
  429. .import_money_secrets(secrets)
  430. .await
  431. .with_context(|| "Failed to import secret keys into wallet")?;
  432. drk.rpc_client.close().await?;
  433. for key in pubkeys {
  434. println!("{}", key);
  435. }
  436. return Ok(())
  437. }
  438. if tree {
  439. let v =
  440. drk.get_money_tree().await.with_context(|| "Failed to fetch Merkle tree")?;
  441. drk.rpc_client.close().await?;
  442. println!("{:#?}", v);
  443. return Ok(())
  444. }
  445. if coins {
  446. let coins = drk
  447. .get_coins(true)
  448. .await
  449. .with_context(|| "Failed to fetch coins from wallet")?;
  450. let aliases_map = drk
  451. .get_aliases_mapped_by_token()
  452. .await
  453. .with_context(|| "Failed to fetch wallet aliases")?;
  454. drk.rpc_client.close().await?;
  455. if coins.is_empty() {
  456. return Ok(())
  457. }
  458. let mut table = Table::new();
  459. table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
  460. table.set_titles(row!["Coin", "Spent", "Token ID", "Aliases", "Value"]);
  461. for coin in coins {
  462. let aliases = match aliases_map.get(&coin.0.note.token_id.to_string()) {
  463. Some(a) => a,
  464. None => "-",
  465. };
  466. table.add_row(row![
  467. format!("{}", bs58::encode(&serialize(&coin.0.coin.inner())).into_string()),
  468. coin.1,
  469. coin.0.note.token_id,
  470. aliases,
  471. format!("{} ({})", coin.0.note.value, encode_base10(coin.0.note.value, 8))
  472. ]);
  473. }
  474. println!("{}", table);
  475. return Ok(())
  476. }
  477. unreachable!()
  478. }
  479. Subcmd::Unspend { coin } => {
  480. let bytes: [u8; 32] = bs58::decode(&coin).into_vec()?.try_into().unwrap();
  481. let elem: pallas::Base = match pallas::Base::from_repr(bytes).into() {
  482. Some(v) => v,
  483. None => return Err(anyhow!("Invalid coin")),
  484. };
  485. let coin = Coin::from(elem);
  486. let drk = Drk::new(args.endpoint).await?;
  487. drk.unspend_coin(&coin).await.with_context(|| "Failed to mark coin as unspent")?;
  488. Ok(())
  489. }
  490. Subcmd::Airdrop { faucet_endpoint, amount, token, address } => {
  491. let amount = f64::from_str(&amount).with_context(|| "Invalid amount")?;
  492. let drk = Drk::new(args.endpoint).await?;
  493. let token_id = drk.get_token(token).await.with_context(|| "Invalid Token ID")?;
  494. let address = match address {
  495. Some(v) => PublicKey::from_str(v.as_str()).with_context(|| "Invalid address")?,
  496. None => drk.wallet_address(1).await.with_context(|| {
  497. "Failed to fetch default address, perhaps the wallet was not initialized?"
  498. })?,
  499. };
  500. let txid = drk
  501. .request_airdrop(faucet_endpoint, amount, token_id, address)
  502. .await
  503. .with_context(|| "Failed to request airdrop")?;
  504. println!("Transaction ID: {}", txid);
  505. Ok(())
  506. }
  507. Subcmd::Transfer { amount, token, recipient, dao, dao_bulla } => {
  508. let _ = f64::from_str(&amount).with_context(|| "Invalid amount")?;
  509. let rcpt = PublicKey::from_str(&recipient).with_context(|| "Invalid recipient")?;
  510. let drk = Drk::new(args.endpoint).await?;
  511. let token_id = drk.get_token(token).await.with_context(|| "Invalid Token ID")?;
  512. let tx = drk
  513. .transfer(&amount, token_id, rcpt, dao, dao_bulla)
  514. .await
  515. .with_context(|| "Failed to create payment transaction")?;
  516. println!("{}", bs58::encode(&serialize(&tx)).into_string());
  517. Ok(())
  518. }
  519. Subcmd::Otc(cmd) => {
  520. let drk = Drk::new(args.endpoint).await?;
  521. match cmd {
  522. OtcSubcmd::Init { value_pair, token_pair } => {
  523. let (vp_send, vp_recv) = parse_value_pair(&value_pair)?;
  524. let (tp_send, tp_recv) = parse_token_pair(&token_pair)?;
  525. let half = drk
  526. .init_swap(vp_send, tp_send, vp_recv, tp_recv)
  527. .await
  528. .with_context(|| "Failed to create swap transaction half")?;
  529. println!("{}", bs58::encode(&serialize(&half)).into_string());
  530. Ok(())
  531. }
  532. OtcSubcmd::Join => {
  533. let mut buf = String::new();
  534. stdin().read_to_string(&mut buf)?;
  535. let bytes = bs58::decode(&buf.trim()).into_vec()?;
  536. let partial: PartialSwapData = deserialize(&bytes)?;
  537. let tx = drk
  538. .join_swap(partial)
  539. .await
  540. .with_context(|| "Failed to create a join swap transaction")?;
  541. println!("{}", bs58::encode(&serialize(&tx)).into_string());
  542. Ok(())
  543. }
  544. OtcSubcmd::Inspect => {
  545. let mut buf = String::new();
  546. stdin().read_to_string(&mut buf)?;
  547. let bytes = bs58::decode(&buf.trim()).into_vec()?;
  548. drk.inspect_swap(bytes).await.with_context(|| "Failed to inspect swap")?;
  549. Ok(())
  550. }
  551. OtcSubcmd::Sign => {
  552. let mut buf = String::new();
  553. stdin().read_to_string(&mut buf)?;
  554. let bytes = bs58::decode(&buf.trim()).into_vec()?;
  555. let mut tx: Transaction = deserialize(&bytes)?;
  556. drk.sign_swap(&mut tx)
  557. .await
  558. .with_context(|| "Failed to sign joined swap transaction")?;
  559. println!("{}", bs58::encode(&serialize(&tx)).into_string());
  560. Ok(())
  561. }
  562. }
  563. }
  564. Subcmd::Inspect => {
  565. let mut buf = String::new();
  566. stdin().read_to_string(&mut buf)?;
  567. let bytes = bs58::decode(&buf.trim()).into_vec()?;
  568. let tx: Transaction = deserialize(&bytes)?;
  569. println!("{:#?}", tx);
  570. Ok(())
  571. }
  572. Subcmd::Broadcast => {
  573. eprintln!("Reading transaction from stdin...");
  574. let mut buf = String::new();
  575. stdin().read_to_string(&mut buf)?;
  576. let bytes = bs58::decode(&buf.trim()).into_vec()?;
  577. let tx = deserialize(&bytes)?;
  578. let drk = Drk::new(args.endpoint).await?;
  579. let txid =
  580. drk.broadcast_tx(&tx).await.with_context(|| "Failed to broadcast transaction")?;
  581. println!("Transaction ID: {}", txid);
  582. Ok(())
  583. }
  584. Subcmd::Subscribe => {
  585. let drk = Drk::new(args.endpoint.clone()).await?;
  586. drk.subscribe_blocks(args.endpoint)
  587. .await
  588. .with_context(|| "Block subscription failed")?;
  589. Ok(())
  590. }
  591. Subcmd::Scan { reset, list, checkpoint } => {
  592. let drk = Drk::new(args.endpoint).await?;
  593. if reset {
  594. eprintln!("Reset requested.");
  595. drk.scan_blocks(true).await.with_context(|| "Failed during scanning")?;
  596. return Ok(())
  597. }
  598. if list {
  599. eprintln!("List requested.");
  600. // TODO: implement
  601. return Ok(())
  602. }
  603. if let Some(c) = checkpoint {
  604. eprintln!("Checkpoint requested: {}", c);
  605. // TODO: implement
  606. return Ok(())
  607. }
  608. drk.scan_blocks(false).await.with_context(|| "Failed during scanning")?;
  609. eprintln!("Finished scanning blockchain");
  610. Ok(())
  611. }
  612. Subcmd::Dao(cmd) => match cmd {
  613. DaoSubcmd::Create { proposer_limit, quorum, approval_ratio, gov_token_id } => {
  614. let _ = f64::from_str(&proposer_limit).with_context(|| "Invalid proposer limit")?;
  615. let _ = f64::from_str(&quorum).with_context(|| "Invalid quorum")?;
  616. let proposer_limit = decode_base10(&proposer_limit, 8, true)?;
  617. let quorum = decode_base10(&quorum, 8, true)?;
  618. if approval_ratio > 1.0 {
  619. eprintln!("Error: Approval ratio cannot be >1.0");
  620. exit(1);
  621. }
  622. let approval_ratio_base = 100_u64;
  623. let approval_ratio_quot = (approval_ratio * approval_ratio_base as f64) as u64;
  624. let drk = Drk::new(args.endpoint).await?;
  625. let gov_token_id =
  626. drk.get_token(gov_token_id).await.with_context(|| "Invalid Token ID")?;
  627. let secret_key = SecretKey::random(&mut OsRng);
  628. let bulla_blind = pallas::Base::random(&mut OsRng);
  629. let dao_params = DaoParams {
  630. proposer_limit,
  631. quorum,
  632. approval_ratio_base,
  633. approval_ratio_quot,
  634. gov_token_id,
  635. secret_key,
  636. bulla_blind,
  637. };
  638. let encoded = bs58::encode(&serialize(&dao_params)).into_string();
  639. println!("{}", encoded);
  640. Ok(())
  641. }
  642. DaoSubcmd::View => {
  643. let mut buf = String::new();
  644. stdin().read_to_string(&mut buf)?;
  645. let bytes = bs58::decode(&buf.trim()).into_vec()?;
  646. let dao_params: DaoParams = deserialize(&bytes)?;
  647. println!("{}", dao_params);
  648. Ok(())
  649. }
  650. DaoSubcmd::Import { dao_name } => {
  651. let mut buf = String::new();
  652. stdin().read_to_string(&mut buf)?;
  653. let bytes = bs58::decode(&buf.trim()).into_vec()?;
  654. let dao_params: DaoParams = deserialize(&bytes)?;
  655. let drk = Drk::new(args.endpoint).await?;
  656. drk.import_dao(dao_name, dao_params)
  657. .await
  658. .with_context(|| "Failed to import DAO")?;
  659. Ok(())
  660. }
  661. DaoSubcmd::List { dao_id } => {
  662. let drk = Drk::new(args.endpoint).await?;
  663. drk.dao_list(dao_id).await.with_context(|| "Failed to list DAO")?;
  664. Ok(())
  665. }
  666. DaoSubcmd::Balance { dao_id } => {
  667. let drk = Drk::new(args.endpoint).await?;
  668. let balmap =
  669. drk.dao_balance(dao_id).await.with_context(|| "Failed to fetch DAO balance")?;
  670. let aliases_map = drk
  671. .get_aliases_mapped_by_token()
  672. .await
  673. .with_context(|| "Failed to fetch wallet aliases")?;
  674. // Create a prettytable with the new data:
  675. let mut table = Table::new();
  676. table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
  677. table.set_titles(row!["Token ID", "Aliases", "Balance"]);
  678. for (token_id, balance) in balmap.iter() {
  679. let aliases = match aliases_map.get(token_id) {
  680. Some(a) => a,
  681. None => "-",
  682. };
  683. // FIXME: Don't hardcode to 8 decimals
  684. table.add_row(row![token_id, aliases, encode_base10(*balance, 8)]);
  685. }
  686. if table.is_empty() {
  687. println!("No unspent balances found");
  688. } else {
  689. println!("{}", table);
  690. }
  691. return Ok(())
  692. }
  693. DaoSubcmd::Mint { dao_id } => {
  694. let drk = Drk::new(args.endpoint).await?;
  695. let tx = drk.dao_mint(dao_id).await.with_context(|| "Failed to mint DAO")?;
  696. println!("{}", bs58::encode(&serialize(&tx)).into_string());
  697. Ok(())
  698. }
  699. DaoSubcmd::Propose { dao_id, recipient, amount, token_id } => {
  700. let _ = f64::from_str(&amount).with_context(|| "Invalid amount")?;
  701. let amount = decode_base10(&amount, 8, true)?;
  702. let rcpt = PublicKey::from_str(&recipient).with_context(|| "Invalid recipient")?;
  703. let drk = Drk::new(args.endpoint).await?;
  704. let token_id = drk.get_token(token_id).await.with_context(|| "Invalid Token ID")?;
  705. let tx = drk
  706. .dao_propose(dao_id, rcpt, amount, token_id)
  707. .await
  708. .with_context(|| "Failed to create DAO proposal")?;
  709. println!("{}", bs58::encode(&serialize(&tx)).into_string());
  710. Ok(())
  711. }
  712. DaoSubcmd::Proposals { dao_id } => {
  713. let drk = Drk::new(args.endpoint).await?;
  714. let proposals = drk.get_dao_proposals(dao_id).await?;
  715. for proposal in proposals {
  716. println!("[{}] {:?}", proposal.id, proposal.bulla());
  717. }
  718. Ok(())
  719. }
  720. DaoSubcmd::Proposal { dao_id, proposal_id } => {
  721. let drk = Drk::new(args.endpoint).await?;
  722. let proposals = drk.get_dao_proposals(dao_id).await?;
  723. let Some(proposal) = proposals.iter().find(|x| x.id == proposal_id) else {
  724. eprintln!("No such DAO proposal found");
  725. exit(1);
  726. };
  727. println!("{}", proposal);
  728. Ok(())
  729. }
  730. DaoSubcmd::Vote { dao_id, proposal_id, vote, vote_weight } => {
  731. let drk = Drk::new(args.endpoint).await?;
  732. let _ = f64::from_str(&vote_weight).with_context(|| "Invalid vote weight")?;
  733. let weight = decode_base10(&vote_weight, 8, true)?;
  734. if vote > 1 {
  735. eprintln!("Vote can be either 0 (NO) or 1 (YES)");
  736. exit(1);
  737. }
  738. let vote = vote != 0;
  739. let tx = drk
  740. .dao_vote(dao_id, proposal_id, vote, weight)
  741. .await
  742. .with_context(|| "Failed to create DAO Vote transaction")?;
  743. // TODO: Write our_vote in the proposal sql.
  744. println!("{}", bs58::encode(&serialize(&tx)).into_string());
  745. Ok(())
  746. }
  747. DaoSubcmd::Exec { dao_id, proposal_id } => {
  748. let drk = Drk::new(args.endpoint).await?;
  749. let dao = drk.get_dao_by_id(dao_id).await?;
  750. let proposal = drk.get_dao_proposal_by_id(proposal_id).await?;
  751. assert!(proposal.dao_bulla == dao.bulla());
  752. let tx = drk
  753. .dao_exec(dao, proposal)
  754. .await
  755. .with_context(|| "Failed to execute DAO proposal")?;
  756. println!("{}", bs58::encode(&serialize(&tx)).into_string());
  757. Ok(())
  758. }
  759. },
  760. Subcmd::Explorer(cmd) => match cmd {
  761. ExplorerSubcmd::FetchTx { tx_hash } => {
  762. let tx_hash = blake3::Hash::from_hex(&tx_hash)?;
  763. let drk = Drk::new(args.endpoint).await?;
  764. let tx = if let Some(tx) =
  765. drk.get_tx(&tx_hash).await.with_context(|| "Failed to fetch transaction")?
  766. {
  767. tx
  768. } else {
  769. eprintln!("Tx not found");
  770. return Ok(())
  771. };
  772. // Make sure the tx is correct
  773. assert_eq!(tx.hash(), tx_hash);
  774. println!("Transaction ID: {}", tx_hash);
  775. let is_err = drk
  776. .was_erroneous_tx(&tx_hash)
  777. .await
  778. .with_context(|| "Failed to get tx state")?;
  779. println!("State: {}", if is_err { "failed" } else { "passed" });
  780. Ok(())
  781. }
  782. ExplorerSubcmd::SimulateTx => {
  783. eprintln!("Reading transaction from stdin...");
  784. let mut buf = String::new();
  785. stdin().read_to_string(&mut buf)?;
  786. let bytes = bs58::decode(&buf.trim()).into_vec()?;
  787. let tx = deserialize(&bytes)?;
  788. let drk = Drk::new(args.endpoint).await?;
  789. let is_valid =
  790. drk.simulate_tx(&tx).await.with_context(|| "Failed to simulate tx")?;
  791. println!("Transaction ID: {}", tx.hash());
  792. println!("State: {}", if is_valid { "valid" } else { "invalid" });
  793. Ok(())
  794. }
  795. },
  796. Subcmd::Alias(cmd) => match cmd {
  797. AliasSubcmd::Add { alias, token } => {
  798. if alias.chars().count() > 5 {
  799. eprintln!("Error: Alias exceeds 5 characters");
  800. return Ok(())
  801. }
  802. let token_id =
  803. TokenId::try_from(token.as_str()).with_context(|| "Invalid Token ID")?;
  804. let drk = Drk::new(args.endpoint).await?;
  805. drk.add_alias(alias, token_id).await?;
  806. Ok(())
  807. }
  808. AliasSubcmd::Show { alias, token } => {
  809. let token_id = match token {
  810. Some(t) => {
  811. Some(TokenId::try_from(t.as_str()).with_context(|| "Invalid Token ID")?)
  812. }
  813. None => None,
  814. };
  815. let drk = Drk::new(args.endpoint).await?;
  816. let map = drk.get_aliases(alias, token_id).await?;
  817. // Create a prettytable with the new data:
  818. let mut table = Table::new();
  819. table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
  820. table.set_titles(row!["Alias", "Token ID"]);
  821. for (alias, token_id) in map.iter() {
  822. table.add_row(row![alias, token_id]);
  823. }
  824. if table.is_empty() {
  825. println!("No aliases found");
  826. } else {
  827. println!("{}", table);
  828. }
  829. Ok(())
  830. }
  831. AliasSubcmd::Remove { alias } => {
  832. let drk = Drk::new(args.endpoint).await?;
  833. drk.remove_alias(alias).await?;
  834. Ok(())
  835. }
  836. },
  837. }
  838. }