main.rs
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 19 use std::{ 20 io::{stdin, Read}, 21 process::exit, 22 str::FromStr, 23 }; 24 25 use prettytable::{format, row, Table}; 26 use rand::rngs::OsRng; 27 use smol::{fs::read_to_string, stream::StreamExt}; 28 use structopt_toml::{serde::Deserialize, structopt::StructOpt, StructOptToml}; 29 use tracing::info; 30 use url::Url; 31 32 use darkfi::{ 33 async_daemonize, cli_desc, 34 system::ExecutorPtr, 35 util::{ 36 encoding::base64, 37 parse::{decode_base10, encode_base10}, 38 path::{expand_path, get_config_path}, 39 }, 40 zk::halo2::Field, 41 Error, Result, 42 }; 43 use darkfi_dao_contract::{blockwindow, model::DaoProposalBulla, DaoFunction}; 44 use darkfi_money_contract::model::{Coin, CoinAttributes, TokenId}; 45 use darkfi_sdk::{ 46 crypto::{ 47 note::AeadEncryptedNote, BaseBlind, ContractId, FuncId, FuncRef, Keypair, PublicKey, 48 SecretKey, DAO_CONTRACT_ID, 49 }, 50 pasta::{group::ff::PrimeField, pallas}, 51 tx::TransactionHash, 52 }; 53 use darkfi_serial::{deserialize_async, serialize_async}; 54 55 use drk::{ 56 cli_util::{ 57 generate_completions, kaching, parse_token_pair, parse_tx_from_stdin, parse_value_pair, 58 print_output, 59 }, 60 dao::{DaoParams, ProposalRecord}, 61 interactive::interactive, 62 money::BALANCE_BASE10_DECIMALS, 63 swap::PartialSwapData, 64 Drk, 65 }; 66 67 const CONFIG_FILE: &str = "drk_config.toml"; 68 const CONFIG_FILE_CONTENTS: &str = include_str!("../drk_config.toml"); 69 70 // Dev Note: when adding/modifying args here, 71 // don't forget to update cli_util::generate_completions() 72 // and interactive::help(). 73 #[derive(Clone, Debug, Deserialize, StructOpt, StructOptToml)] 74 #[serde(default)] 75 #[structopt(name = "drk", about = cli_desc!())] 76 struct Args { 77 #[structopt(short, long)] 78 /// Configuration file to use 79 config: Option<String>, 80 81 #[structopt(short, long, default_value = "testnet")] 82 /// Blockchain network to use 83 network: String, 84 85 #[structopt(subcommand)] 86 /// Sub command to execute 87 command: Subcmd, 88 89 #[structopt(short, long)] 90 /// Flag indicating whether you want some fun in your life 91 fun: bool, 92 93 #[structopt(short, long)] 94 /// Set log file to ouput into 95 log: Option<String>, 96 97 #[structopt(short, parse(from_occurrences))] 98 /// Increase verbosity (-vvv supported) 99 verbose: u8, 100 } 101 102 // Dev Note: when adding/modifying commands here, 103 // don't forget to update cli_util::generate_completions() 104 #[derive(Clone, Debug, Deserialize, StructOpt)] 105 enum Subcmd { 106 /// Enter Drk interactive shell 107 Interactive, 108 109 /// Fun 110 Kaching, 111 112 /// Send a ping request to the darkfid RPC endpoint 113 Ping, 114 115 /// Generate a SHELL completion script and print to stdout 116 Completions { 117 /// The Shell you want to generate script for 118 shell: String, 119 }, 120 121 /// Wallet operations 122 Wallet { 123 #[structopt(subcommand)] 124 /// Sub command to execute 125 command: WalletSubcmd, 126 }, 127 128 /// Read a transaction from stdin and mark its input coins as spent 129 Spend, 130 131 /// Unspend a coin 132 Unspend { 133 /// base64-encoded coin to mark as unspent 134 coin: String, 135 }, 136 137 /// Create a payment transaction 138 Transfer { 139 /// Amount to send 140 amount: String, 141 142 /// Token ID to send 143 token: String, 144 145 /// Recipient address 146 recipient: String, 147 148 /// Optional contract spend hook to use 149 spend_hook: Option<String>, 150 151 /// Optional user data to use 152 user_data: Option<String>, 153 154 #[structopt(long)] 155 /// Split the output coin into two equal halves 156 half_split: bool, 157 }, 158 159 /// OTC atomic swap 160 Otc { 161 #[structopt(subcommand)] 162 /// Sub command to execute 163 command: OtcSubcmd, 164 }, 165 166 /// DAO functionalities 167 Dao { 168 #[structopt(subcommand)] 169 /// Sub command to execute 170 command: DaoSubcmd, 171 }, 172 173 /// Attach the fee call to a transaction given from stdin 174 AttachFee, 175 176 /// Inspect a transaction from stdin 177 Inspect, 178 179 /// Read a transaction from stdin and broadcast it 180 Broadcast, 181 182 /// Scan the blockchain and parse relevant transactions 183 Scan { 184 #[structopt(long)] 185 /// Reset wallet state to provided block height and start scanning 186 reset: Option<u32>, 187 }, 188 189 /// Explorer related subcommands 190 Explorer { 191 #[structopt(subcommand)] 192 /// Sub command to execute 193 command: ExplorerSubcmd, 194 }, 195 196 /// Manage Token aliases 197 Alias { 198 #[structopt(subcommand)] 199 /// Sub command to execute 200 command: AliasSubcmd, 201 }, 202 203 /// Token functionalities 204 Token { 205 #[structopt(subcommand)] 206 /// Sub command to execute 207 command: TokenSubcmd, 208 }, 209 210 /// Contract functionalities 211 Contract { 212 #[structopt(subcommand)] 213 /// Sub command to execute 214 command: ContractSubcmd, 215 }, 216 } 217 218 #[derive(Clone, Debug, Deserialize, StructOpt)] 219 enum WalletSubcmd { 220 /// Initialize wallet database 221 Initialize, 222 223 /// Generate a new keypair in the wallet 224 Keygen, 225 226 /// Query the wallet for known balances 227 Balance, 228 229 /// Get the default address in the wallet 230 Address, 231 232 /// Print all the addresses in the wallet 233 Addresses, 234 235 /// Set the default address in the wallet 236 DefaultAddress { 237 /// Identifier of the address 238 index: usize, 239 }, 240 241 /// Print all the secret keys from the wallet 242 Secrets, 243 244 /// Import secret keys from stdin into the wallet, separated by newlines 245 ImportSecrets, 246 247 /// Print the Merkle tree in the wallet 248 Tree, 249 250 /// Print all the coins in the wallet 251 Coins, 252 } 253 254 #[derive(Clone, Debug, Deserialize, StructOpt)] 255 enum OtcSubcmd { 256 /// Initialize the first half of the atomic swap 257 Init { 258 /// Value pair to send:recv (11.55:99.42) 259 #[structopt(short, long)] 260 value_pair: String, 261 262 /// Token pair to send:recv (f00:b4r) 263 #[structopt(short, long)] 264 token_pair: String, 265 }, 266 267 /// Build entire swap tx given the first half from stdin 268 Join, 269 270 /// Inspect a swap half or the full swap tx from stdin 271 Inspect, 272 273 /// Sign a swap transaction given from stdin 274 Sign, 275 } 276 277 #[derive(Clone, Debug, Deserialize, StructOpt)] 278 enum DaoSubcmd { 279 /// Create DAO parameters 280 Create { 281 /// The minimum amount of governance tokens needed to open a proposal for this DAO 282 proposer_limit: String, 283 /// Minimal threshold of participating total tokens needed for a proposal to pass 284 quorum: String, 285 /// Minimal threshold of participating total tokens needed for a proposal to 286 /// be considered as strongly supported, enabling early execution. 287 /// Must be greater or equal to normal quorum. 288 early_exec_quorum: String, 289 /// The ratio of winning votes/total votes needed for a proposal to pass (2 decimals) 290 approval_ratio: f64, 291 /// DAO's governance token ID 292 gov_token_id: String, 293 }, 294 295 /// View DAO data from stdin 296 View, 297 298 /// Import DAO data from stdin 299 Import { 300 /// Name identifier for the DAO 301 name: String, 302 }, 303 304 /// List imported DAOs (or info about a specific one) 305 List { 306 /// Name identifier for the DAO (optional) 307 name: Option<String>, 308 }, 309 310 /// Show the balance of a DAO 311 Balance { 312 /// Name identifier for the DAO 313 name: String, 314 }, 315 316 /// Mint an imported DAO on-chain 317 Mint { 318 /// Name identifier for the DAO 319 name: String, 320 }, 321 322 /// Create a transfer proposal for a DAO 323 ProposeTransfer { 324 /// Name identifier for the DAO 325 name: String, 326 327 /// Duration of the proposal, in block windows 328 duration: u64, 329 330 /// Amount to send 331 amount: String, 332 333 /// Token ID to send 334 token: String, 335 336 /// Recipient address 337 recipient: String, 338 339 /// Optional contract spend hook to use 340 spend_hook: Option<String>, 341 342 /// Optional user data to use 343 user_data: Option<String>, 344 }, 345 346 /// Create a generic proposal for a DAO 347 ProposeGeneric { 348 /// Name identifier for the DAO 349 name: String, 350 351 /// Duration of the proposal, in block windows 352 duration: u64, 353 354 /// Optional user data to use 355 user_data: Option<String>, 356 }, 357 358 /// List DAO proposals 359 Proposals { 360 /// Name identifier for the DAO 361 name: String, 362 }, 363 364 /// View a DAO proposal data 365 Proposal { 366 /// Bulla identifier for the proposal 367 bulla: String, 368 369 #[structopt(long)] 370 /// Encrypt the proposal and encode it to base64 371 export: bool, 372 373 #[structopt(long)] 374 /// Create the proposal transaction 375 mint_proposal: bool, 376 }, 377 378 /// Import a base64 encoded and encrypted proposal from stdin 379 ProposalImport, 380 381 /// Vote on a given proposal 382 Vote { 383 /// Bulla identifier for the proposal 384 bulla: String, 385 386 /// Vote (0 for NO, 1 for YES) 387 vote: u8, 388 389 /// Optional vote weight (amount of governance tokens) 390 vote_weight: Option<String>, 391 }, 392 393 /// Execute a DAO proposal 394 Exec { 395 /// Bulla identifier for the proposal 396 bulla: String, 397 398 #[structopt(long)] 399 /// Execute the proposal early 400 early: bool, 401 }, 402 403 /// Print the DAO contract base64-encoded spend hook 404 SpendHook, 405 } 406 407 #[derive(Clone, Debug, Deserialize, StructOpt)] 408 enum ExplorerSubcmd { 409 /// Fetch a blockchain transaction by hash 410 FetchTx { 411 /// Transaction hash 412 tx_hash: String, 413 414 #[structopt(long)] 415 /// Encode transaction to base64 416 encode: bool, 417 }, 418 419 /// Read a transaction from stdin and simulate it 420 SimulateTx, 421 422 /// Fetch broadcasted transactions history 423 TxsHistory { 424 /// Fetch specific history record (optional) 425 tx_hash: Option<String>, 426 427 #[structopt(long)] 428 /// Encode specific history record transaction to base64 429 encode: bool, 430 }, 431 432 /// Remove reverted transactions from history 433 ClearReverted, 434 435 /// Fetch scanned blocks records 436 ScannedBlocks { 437 /// Fetch specific height record (optional) 438 height: Option<u32>, 439 }, 440 } 441 442 #[derive(Clone, Debug, Deserialize, StructOpt)] 443 enum AliasSubcmd { 444 /// Create a Token alias 445 Add { 446 /// Token alias 447 alias: String, 448 449 /// Token to create alias for 450 token: String, 451 }, 452 453 /// Print alias info of optional arguments. 454 /// If no argument is provided, list all the aliases in the wallet. 455 Show { 456 /// Token alias to search for 457 #[structopt(short, long)] 458 alias: Option<String>, 459 460 /// Token to search alias for 461 #[structopt(short, long)] 462 token: Option<String>, 463 }, 464 465 /// Remove a Token alias 466 Remove { 467 /// Token alias to remove 468 alias: String, 469 }, 470 } 471 472 #[derive(Clone, Debug, Deserialize, StructOpt)] 473 enum TokenSubcmd { 474 /// Import a mint authority 475 Import { 476 /// Mint authority secret key 477 secret_key: String, 478 479 /// Mint authority token blind 480 token_blind: String, 481 }, 482 483 /// Generate a new mint authority 484 GenerateMint, 485 486 /// List token IDs with available mint authorities 487 List, 488 489 /// Mint tokens 490 Mint { 491 /// Token ID to mint 492 token: String, 493 494 /// Amount to mint 495 amount: String, 496 497 /// Recipient of the minted tokens 498 recipient: String, 499 500 /// Optional contract spend hook to use 501 spend_hook: Option<String>, 502 503 /// Optional user data to use 504 user_data: Option<String>, 505 }, 506 507 /// Freeze a token mint 508 Freeze { 509 /// Token ID to freeze 510 token: String, 511 }, 512 } 513 514 #[derive(Clone, Debug, Deserialize, StructOpt)] 515 enum ContractSubcmd { 516 /// Generate a new deploy authority 517 GenerateDeploy, 518 519 /// List deploy authorities in the wallet (or a specific one) 520 List { 521 /// Contract ID (optional) 522 contract_id: Option<String>, 523 }, 524 525 /// Export a contract history record wasm bincode and deployment instruction, encoded to base64 526 ExportData { 527 /// Record transaction hash 528 tx_hash: String, 529 }, 530 531 /// Deploy a smart contract 532 Deploy { 533 /// Contract ID (deploy authority) 534 deploy_auth: String, 535 536 /// Path to contract wasm bincode 537 wasm_path: String, 538 539 /// Optional path to serialized deploy instruction 540 deploy_ix: Option<String>, 541 }, 542 543 /// Lock a smart contract 544 Lock { 545 /// Contract ID (deploy authority) 546 deploy_auth: String, 547 }, 548 } 549 550 /// Defines a blockchain network configuration. 551 /// Default values correspond to a local network. 552 #[derive(Clone, Debug, serde::Deserialize, structopt::StructOpt, structopt_toml::StructOptToml)] 553 #[structopt()] 554 struct BlockchainNetwork { 555 #[structopt(long, default_value = "~/.local/share/darkfi/drk/localnet/cache")] 556 /// Path to blockchain cache database 557 cache_path: String, 558 559 #[structopt(long, default_value = "~/.local/share/darkfi/drk/localnet/wallet.db")] 560 /// Path to wallet database 561 wallet_path: String, 562 563 #[structopt(long, default_value = "changeme")] 564 /// Password for the wallet database 565 wallet_pass: String, 566 567 #[structopt(short, long, default_value = "tcp://127.0.0.1:8240")] 568 /// darkfid JSON-RPC endpoint 569 endpoint: Url, 570 571 #[structopt(long, default_value = "~/.local/share/darkfi/drk/localnet/history.txt")] 572 /// Path to interactive shell history file 573 history_path: String, 574 } 575 576 /// Auxiliary function to parse darkfid configuration file and extract requested 577 /// blockchain network config. 578 async fn parse_blockchain_config( 579 config: Option<String>, 580 network: &str, 581 ) -> Result<BlockchainNetwork> { 582 // Grab config path 583 let config_path = get_config_path(config, CONFIG_FILE)?; 584 585 // Parse TOML file contents 586 let contents = read_to_string(&config_path).await?; 587 let contents: toml::Value = match toml::from_str(&contents) { 588 Ok(v) => v, 589 Err(e) => { 590 eprintln!("Failed parsing TOML config: {e}"); 591 return Err(Error::ParseFailed("Failed parsing TOML config")) 592 } 593 }; 594 595 // Grab requested network config 596 let Some(table) = contents.as_table() else { return Err(Error::ParseFailed("TOML not a map")) }; 597 let Some(network_configs) = table.get("network_config") else { 598 return Err(Error::ParseFailed("TOML does not contain network configurations")) 599 }; 600 let Some(network_configs) = network_configs.as_table() else { 601 return Err(Error::ParseFailed("`network_config` not a map")) 602 }; 603 let Some(network_config) = network_configs.get(network) else { 604 return Err(Error::ParseFailed("TOML does not contain requested network configuration")) 605 }; 606 let network_config = toml::to_string(&network_config).unwrap(); 607 let network_config = 608 match BlockchainNetwork::from_iter_with_toml::<Vec<String>>(&network_config, vec![]) { 609 Ok(v) => v, 610 Err(e) => { 611 eprintln!("Failed parsing requested network configuration: {e}"); 612 return Err(Error::ParseFailed("Failed parsing requested network configuration")) 613 } 614 }; 615 616 Ok(network_config) 617 } 618 619 /// Auxiliary function to create a `Drk` wallet for provided configuration. 620 async fn new_wallet( 621 cache_path: String, 622 wallet_path: String, 623 wallet_pass: String, 624 endpoint: Option<Url>, 625 ex: &ExecutorPtr, 626 fun: bool, 627 ) -> Drk { 628 // Script kiddies protection 629 if wallet_pass == "changeme" { 630 eprintln!("Please don't use default wallet password..."); 631 exit(2); 632 } 633 634 match Drk::new(cache_path, wallet_path, wallet_pass, endpoint, ex, fun).await { 635 Ok(wallet) => wallet, 636 Err(e) => { 637 eprintln!("Error initializing wallet: {e}"); 638 exit(2); 639 } 640 } 641 } 642 643 async_daemonize!(realmain); 644 async fn realmain(args: Args, ex: ExecutorPtr) -> Result<()> { 645 // Grab blockchain network configuration 646 let blockchain_config = match args.network.as_str() { 647 "localnet" => parse_blockchain_config(args.config, "localnet").await?, 648 "testnet" => parse_blockchain_config(args.config, "testnet").await?, 649 "mainnet" => parse_blockchain_config(args.config, "mainnet").await?, 650 _ => { 651 eprintln!("Unsupported chain `{}`", args.network); 652 return Err(Error::UnsupportedChain) 653 } 654 }; 655 656 match args.command { 657 Subcmd::Interactive => { 658 let drk = new_wallet( 659 blockchain_config.cache_path, 660 blockchain_config.wallet_path, 661 blockchain_config.wallet_pass, 662 Some(blockchain_config.endpoint.clone()), 663 &ex, 664 args.fun, 665 ) 666 .await 667 .into_ptr(); 668 interactive(&drk, &blockchain_config.endpoint, &blockchain_config.history_path, &ex) 669 .await; 670 drk.read().await.stop_rpc_client().await?; 671 Ok(()) 672 } 673 674 Subcmd::Kaching => { 675 if !args.fun { 676 println!("Apparently you don't like fun..."); 677 return Ok(()) 678 } 679 kaching().await; 680 Ok(()) 681 } 682 683 Subcmd::Ping => { 684 let drk = new_wallet( 685 blockchain_config.cache_path, 686 blockchain_config.wallet_path, 687 blockchain_config.wallet_pass, 688 Some(blockchain_config.endpoint), 689 &ex, 690 args.fun, 691 ) 692 .await; 693 let mut output = vec![]; 694 if let Err(e) = drk.ping(&mut output).await { 695 print_output(&output); 696 return Err(e) 697 }; 698 print_output(&output); 699 drk.stop_rpc_client().await 700 } 701 702 Subcmd::Completions { shell } => { 703 println!("{}", generate_completions(&shell)?); 704 Ok(()) 705 } 706 707 Subcmd::Wallet { command } => { 708 let drk = new_wallet( 709 blockchain_config.cache_path, 710 blockchain_config.wallet_path, 711 blockchain_config.wallet_pass, 712 None, 713 &ex, 714 args.fun, 715 ) 716 .await; 717 718 match command { 719 WalletSubcmd::Initialize => { 720 if let Err(e) = drk.initialize_wallet().await { 721 eprintln!("Error initializing wallet: {e}"); 722 exit(2); 723 } 724 let mut output = vec![]; 725 if let Err(e) = drk.initialize_money(&mut output).await { 726 print_output(&output); 727 eprintln!("Failed to initialize Money: {e}"); 728 exit(2); 729 } 730 print_output(&output); 731 if let Err(e) = drk.initialize_dao().await { 732 eprintln!("Failed to initialize DAO: {e}"); 733 exit(2); 734 } 735 if let Err(e) = drk.initialize_deployooor() { 736 eprintln!("Failed to initialize Deployooor: {e}"); 737 exit(2); 738 } 739 } 740 741 WalletSubcmd::Keygen => { 742 let mut output = vec![]; 743 if let Err(e) = drk.money_keygen(&mut output).await { 744 print_output(&output); 745 eprintln!("Failed to generate keypair: {e}"); 746 exit(2); 747 } 748 print_output(&output); 749 } 750 751 WalletSubcmd::Balance => { 752 let balmap = drk.money_balance().await?; 753 754 let aliases_map = drk.get_aliases_mapped_by_token().await?; 755 756 // Create a prettytable with the new data: 757 let mut table = Table::new(); 758 table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); 759 table.set_titles(row!["Token ID", "Aliases", "Balance"]); 760 for (token_id, balance) in balmap.iter() { 761 let aliases = match aliases_map.get(token_id) { 762 Some(a) => a, 763 None => "-", 764 }; 765 766 table.add_row(row![ 767 token_id, 768 aliases, 769 encode_base10(*balance, BALANCE_BASE10_DECIMALS) 770 ]); 771 } 772 773 if table.is_empty() { 774 println!("No unspent balances found"); 775 } else { 776 println!("{table}"); 777 } 778 } 779 780 WalletSubcmd::Address => match drk.default_address().await { 781 Ok(address) => println!("{address}"), 782 Err(e) => { 783 eprintln!("Failed to fetch default address: {e}"); 784 exit(2); 785 } 786 }, 787 788 WalletSubcmd::Addresses => { 789 let addresses = drk.addresses().await?; 790 791 // Create a prettytable with the new data: 792 let mut table = Table::new(); 793 table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); 794 table.set_titles(row!["Key ID", "Public Key", "Secret Key", "Is Default"]); 795 for (key_id, public_key, secret_key, is_default) in addresses { 796 let is_default = match is_default { 797 1 => "*", 798 _ => "", 799 }; 800 table.add_row(row![key_id, public_key, secret_key, is_default]); 801 } 802 803 if table.is_empty() { 804 println!("No addresses found"); 805 } else { 806 println!("{table}"); 807 } 808 } 809 810 WalletSubcmd::DefaultAddress { index } => { 811 if let Err(e) = drk.set_default_address(index) { 812 eprintln!("Failed to set default address: {e}"); 813 exit(2); 814 } 815 } 816 817 WalletSubcmd::Secrets => { 818 for secret in drk.get_money_secrets().await? { 819 println!("{secret}"); 820 } 821 } 822 823 WalletSubcmd::ImportSecrets => { 824 let mut secrets = vec![]; 825 let lines = stdin().lines(); 826 for (i, line) in lines.enumerate() { 827 if let Ok(line) = line { 828 let bytes = bs58::decode(&line.trim()).into_vec()?; 829 let Ok(secret) = deserialize_async(&bytes).await else { 830 println!("Warning: Failed to deserialize secret on line {i}"); 831 continue 832 }; 833 secrets.push(secret); 834 } 835 } 836 837 let mut output = vec![]; 838 let pubkeys = match drk.import_money_secrets(secrets, &mut output).await { 839 Ok(p) => { 840 print_output(&output); 841 p 842 } 843 Err(e) => { 844 print_output(&output); 845 eprintln!("Failed to import secret keys into wallet: {e}"); 846 exit(2); 847 } 848 }; 849 850 for key in pubkeys { 851 println!("{key}"); 852 } 853 } 854 855 WalletSubcmd::Tree => { 856 println!("{:#?}", drk.get_money_tree().await?); 857 } 858 859 WalletSubcmd::Coins => { 860 let coins = drk.get_coins(true).await?; 861 862 if coins.is_empty() { 863 return Ok(()) 864 } 865 866 let aliases_map = drk.get_aliases_mapped_by_token().await?; 867 868 let mut table = Table::new(); 869 table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); 870 table.set_titles(row![ 871 "Coin", 872 "Token ID", 873 "Aliases", 874 "Value", 875 "Spend Hook", 876 "User Data", 877 "Creation Height", 878 "Spent", 879 "Spent Height", 880 "Spent TX" 881 ]); 882 for coin in coins { 883 let aliases = match aliases_map.get(&coin.0.note.token_id.to_string()) { 884 Some(a) => a, 885 None => "-", 886 }; 887 888 let spend_hook = if coin.0.note.spend_hook != FuncId::none() { 889 format!("{}", coin.0.note.spend_hook) 890 } else { 891 String::from("-") 892 }; 893 894 let user_data = if coin.0.note.user_data != pallas::Base::ZERO { 895 bs58::encode(&serialize_async(&coin.0.note.user_data).await) 896 .into_string() 897 .to_string() 898 } else { 899 String::from("-") 900 }; 901 902 let spent_height = match coin.3 { 903 Some(spent_height) => spent_height.to_string(), 904 None => String::from("-"), 905 }; 906 907 table.add_row(row![ 908 bs58::encode(&serialize_async(&coin.0.coin.inner()).await) 909 .into_string() 910 .to_string(), 911 coin.0.note.token_id, 912 aliases, 913 format!( 914 "{} ({})", 915 coin.0.note.value, 916 encode_base10(coin.0.note.value, BALANCE_BASE10_DECIMALS) 917 ), 918 spend_hook, 919 user_data, 920 coin.1, 921 coin.2, 922 spent_height, 923 coin.4, 924 ]); 925 } 926 927 println!("{table}"); 928 } 929 } 930 931 Ok(()) 932 } 933 934 Subcmd::Spend => { 935 let tx = parse_tx_from_stdin().await?; 936 937 let drk = new_wallet( 938 blockchain_config.cache_path, 939 blockchain_config.wallet_path, 940 blockchain_config.wallet_pass, 941 None, 942 &ex, 943 args.fun, 944 ) 945 .await; 946 947 let mut output = vec![]; 948 if let Err(e) = drk.mark_tx_spend(&tx, &mut output).await { 949 print_output(&output); 950 eprintln!("Failed to mark transaction coins as spent: {e}"); 951 exit(2); 952 }; 953 print_output(&output); 954 955 Ok(()) 956 } 957 958 Subcmd::Unspend { coin } => { 959 let bytes: [u8; 32] = match bs58::decode(&coin).into_vec()?.try_into() { 960 Ok(b) => b, 961 Err(e) => { 962 eprintln!("Invalid coin: {e:?}"); 963 exit(2); 964 } 965 }; 966 967 let elem: pallas::Base = match pallas::Base::from_repr(bytes).into() { 968 Some(v) => v, 969 None => { 970 eprintln!("Invalid coin"); 971 exit(2); 972 } 973 }; 974 975 let coin = Coin::from(elem); 976 let drk = new_wallet( 977 blockchain_config.cache_path, 978 blockchain_config.wallet_path, 979 blockchain_config.wallet_pass, 980 None, 981 &ex, 982 args.fun, 983 ) 984 .await; 985 if let Err(e) = drk.unspend_coin(&coin).await { 986 eprintln!("Failed to mark coin as unspent: {e}"); 987 exit(2); 988 } 989 990 Ok(()) 991 } 992 993 Subcmd::Transfer { amount, token, recipient, spend_hook, user_data, half_split } => { 994 let drk = new_wallet( 995 blockchain_config.cache_path, 996 blockchain_config.wallet_path, 997 blockchain_config.wallet_pass, 998 Some(blockchain_config.endpoint), 999 &ex, 1000 args.fun, 1001 ) 1002 .await; 1003 1004 if let Err(e) = f64::from_str(&amount) { 1005 eprintln!("Invalid amount: {e}"); 1006 exit(2); 1007 } 1008 1009 let rcpt = match PublicKey::from_str(&recipient) { 1010 Ok(r) => r, 1011 Err(e) => { 1012 eprintln!("Invalid recipient: {e}"); 1013 exit(2); 1014 } 1015 }; 1016 1017 let token_id = match drk.get_token(token).await { 1018 Ok(t) => t, 1019 Err(e) => { 1020 eprintln!("Invalid token alias: {e}"); 1021 exit(2); 1022 } 1023 }; 1024 1025 let spend_hook = match spend_hook { 1026 Some(s) => match FuncId::from_str(&s) { 1027 Ok(s) => Some(s), 1028 Err(e) => { 1029 eprintln!("Invalid spend hook: {e}"); 1030 exit(2); 1031 } 1032 }, 1033 None => None, 1034 }; 1035 1036 let user_data = match user_data { 1037 Some(u) => { 1038 let bytes: [u8; 32] = match bs58::decode(&u).into_vec()?.try_into() { 1039 Ok(b) => b, 1040 Err(e) => { 1041 eprintln!("Invalid user data: {e:?}"); 1042 exit(2); 1043 } 1044 }; 1045 1046 match pallas::Base::from_repr(bytes).into() { 1047 Some(v) => Some(v), 1048 None => { 1049 eprintln!("Invalid user data"); 1050 exit(2); 1051 } 1052 } 1053 } 1054 None => None, 1055 }; 1056 1057 let tx = match drk 1058 .transfer(&amount, token_id, rcpt, spend_hook, user_data, half_split) 1059 .await 1060 { 1061 Ok(t) => t, 1062 Err(e) => { 1063 eprintln!("Failed to create payment transaction: {e}"); 1064 exit(2); 1065 } 1066 }; 1067 1068 println!("{}", base64::encode(&serialize_async(&tx).await)); 1069 1070 drk.stop_rpc_client().await 1071 } 1072 1073 Subcmd::Otc { command } => match command { 1074 OtcSubcmd::Init { value_pair, token_pair } => { 1075 let drk = new_wallet( 1076 blockchain_config.cache_path, 1077 blockchain_config.wallet_path, 1078 blockchain_config.wallet_pass, 1079 Some(blockchain_config.endpoint), 1080 &ex, 1081 args.fun, 1082 ) 1083 .await; 1084 let value_pair = parse_value_pair(&value_pair)?; 1085 let token_pair = parse_token_pair(&drk, &token_pair).await?; 1086 1087 let half = match drk.init_swap(value_pair, token_pair, None, None, None).await { 1088 Ok(h) => h, 1089 Err(e) => { 1090 eprintln!("Failed to create swap transaction half: {e}"); 1091 exit(2); 1092 } 1093 }; 1094 1095 println!("{}", base64::encode(&serialize_async(&half).await)); 1096 drk.stop_rpc_client().await 1097 } 1098 1099 OtcSubcmd::Join => { 1100 let mut buf = String::new(); 1101 stdin().read_to_string(&mut buf)?; 1102 let Some(bytes) = base64::decode(buf.trim()) else { 1103 eprintln!("Failed to decode partial swap data"); 1104 exit(2); 1105 }; 1106 1107 let partial: PartialSwapData = deserialize_async(&bytes).await?; 1108 1109 let drk = new_wallet( 1110 blockchain_config.cache_path, 1111 blockchain_config.wallet_path, 1112 blockchain_config.wallet_pass, 1113 Some(blockchain_config.endpoint), 1114 &ex, 1115 args.fun, 1116 ) 1117 .await; 1118 let tx = match drk.join_swap(partial, None, None, None).await { 1119 Ok(tx) => tx, 1120 Err(e) => { 1121 eprintln!("Failed to create a join swap transaction: {e}"); 1122 exit(2); 1123 } 1124 }; 1125 1126 println!("{}", base64::encode(&serialize_async(&tx).await)); 1127 drk.stop_rpc_client().await 1128 } 1129 1130 OtcSubcmd::Inspect => { 1131 let mut buf = String::new(); 1132 stdin().read_to_string(&mut buf)?; 1133 let Some(bytes) = base64::decode(buf.trim()) else { 1134 eprintln!("Failed to decode swap transaction"); 1135 exit(2); 1136 }; 1137 1138 let drk = new_wallet( 1139 blockchain_config.cache_path, 1140 blockchain_config.wallet_path, 1141 blockchain_config.wallet_pass, 1142 None, 1143 &ex, 1144 args.fun, 1145 ) 1146 .await; 1147 let mut output = vec![]; 1148 if let Err(e) = drk.inspect_swap(bytes, &mut output).await { 1149 print_output(&output); 1150 eprintln!("Failed to inspect swap: {e}"); 1151 exit(2); 1152 }; 1153 print_output(&output); 1154 1155 Ok(()) 1156 } 1157 1158 OtcSubcmd::Sign => { 1159 let mut tx = parse_tx_from_stdin().await?; 1160 1161 let drk = new_wallet( 1162 blockchain_config.cache_path, 1163 blockchain_config.wallet_path, 1164 blockchain_config.wallet_pass, 1165 None, 1166 &ex, 1167 args.fun, 1168 ) 1169 .await; 1170 if let Err(e) = drk.sign_swap(&mut tx).await { 1171 eprintln!("Failed to sign joined swap transaction: {e}"); 1172 exit(2); 1173 }; 1174 1175 println!("{}", base64::encode(&serialize_async(&tx).await)); 1176 Ok(()) 1177 } 1178 }, 1179 1180 Subcmd::Dao { command } => match command { 1181 DaoSubcmd::Create { 1182 proposer_limit, 1183 quorum, 1184 early_exec_quorum, 1185 approval_ratio, 1186 gov_token_id, 1187 } => { 1188 if let Err(e) = f64::from_str(&proposer_limit) { 1189 eprintln!("Invalid proposer limit: {e}"); 1190 exit(2); 1191 } 1192 if let Err(e) = f64::from_str(&quorum) { 1193 eprintln!("Invalid quorum: {e}"); 1194 exit(2); 1195 } 1196 if let Err(e) = f64::from_str(&early_exec_quorum) { 1197 eprintln!("Invalid early exec quorum: {e}"); 1198 exit(2); 1199 } 1200 1201 let proposer_limit = decode_base10(&proposer_limit, BALANCE_BASE10_DECIMALS, true)?; 1202 let quorum = decode_base10(&quorum, BALANCE_BASE10_DECIMALS, true)?; 1203 let early_exec_quorum = 1204 decode_base10(&early_exec_quorum, BALANCE_BASE10_DECIMALS, true)?; 1205 1206 if approval_ratio > 1.0 { 1207 eprintln!("Error: Approval ratio cannot be >1.0"); 1208 exit(2); 1209 } 1210 1211 let approval_ratio_base = 100_u64; 1212 let approval_ratio_quot = (approval_ratio * approval_ratio_base as f64) as u64; 1213 1214 let drk = new_wallet( 1215 blockchain_config.cache_path, 1216 blockchain_config.wallet_path, 1217 blockchain_config.wallet_pass, 1218 None, 1219 &ex, 1220 args.fun, 1221 ) 1222 .await; 1223 let gov_token_id = match drk.get_token(gov_token_id).await { 1224 Ok(g) => g, 1225 Err(e) => { 1226 eprintln!("Invalid Token ID: {e}"); 1227 exit(2); 1228 } 1229 }; 1230 1231 let notes_keypair = Keypair::random(&mut OsRng); 1232 let proposer_keypair = Keypair::random(&mut OsRng); 1233 let proposals_keypair = Keypair::random(&mut OsRng); 1234 let votes_keypair = Keypair::random(&mut OsRng); 1235 let exec_keypair = Keypair::random(&mut OsRng); 1236 let early_exec_keypair = Keypair::random(&mut OsRng); 1237 let bulla_blind = BaseBlind::random(&mut OsRng); 1238 1239 let params = DaoParams::new( 1240 proposer_limit, 1241 quorum, 1242 early_exec_quorum, 1243 approval_ratio_base, 1244 approval_ratio_quot, 1245 gov_token_id, 1246 Some(notes_keypair.secret), 1247 notes_keypair.public, 1248 Some(proposer_keypair.secret), 1249 proposer_keypair.public, 1250 Some(proposals_keypair.secret), 1251 proposals_keypair.public, 1252 Some(votes_keypair.secret), 1253 votes_keypair.public, 1254 Some(exec_keypair.secret), 1255 exec_keypair.public, 1256 Some(early_exec_keypair.secret), 1257 early_exec_keypair.public, 1258 bulla_blind, 1259 ); 1260 1261 println!("{}", params.toml_str()); 1262 1263 Ok(()) 1264 } 1265 1266 DaoSubcmd::View => { 1267 let mut buf = String::new(); 1268 stdin().read_to_string(&mut buf)?; 1269 let params = DaoParams::from_toml_str(&buf)?; 1270 println!("{params}"); 1271 1272 Ok(()) 1273 } 1274 1275 DaoSubcmd::Import { name } => { 1276 let mut buf = String::new(); 1277 stdin().read_to_string(&mut buf)?; 1278 let params = DaoParams::from_toml_str(&buf)?; 1279 1280 let drk = new_wallet( 1281 blockchain_config.cache_path, 1282 blockchain_config.wallet_path, 1283 blockchain_config.wallet_pass, 1284 None, 1285 &ex, 1286 args.fun, 1287 ) 1288 .await; 1289 let mut output = vec![]; 1290 if let Err(e) = drk.import_dao(&name, ¶ms, &mut output).await { 1291 print_output(&output); 1292 eprintln!("Failed to import DAO: {e}"); 1293 exit(2); 1294 } 1295 print_output(&output); 1296 1297 Ok(()) 1298 } 1299 1300 DaoSubcmd::List { name } => { 1301 let drk = new_wallet( 1302 blockchain_config.cache_path, 1303 blockchain_config.wallet_path, 1304 blockchain_config.wallet_pass, 1305 None, 1306 &ex, 1307 args.fun, 1308 ) 1309 .await; 1310 let mut output = vec![]; 1311 if let Err(e) = drk.dao_list(&name, &mut output).await { 1312 print_output(&output); 1313 eprintln!("Failed to list DAO: {e}"); 1314 exit(2); 1315 } 1316 print_output(&output); 1317 1318 Ok(()) 1319 } 1320 1321 DaoSubcmd::Balance { name } => { 1322 let drk = new_wallet( 1323 blockchain_config.cache_path, 1324 blockchain_config.wallet_path, 1325 blockchain_config.wallet_pass, 1326 None, 1327 &ex, 1328 args.fun, 1329 ) 1330 .await; 1331 let balmap = match drk.dao_balance(&name).await { 1332 Ok(b) => b, 1333 Err(e) => { 1334 eprintln!("Failed to fetch DAO balance: {e}"); 1335 exit(2); 1336 } 1337 }; 1338 1339 let aliases_map = match drk.get_aliases_mapped_by_token().await { 1340 Ok(a) => a, 1341 Err(e) => { 1342 eprintln!("Failed to fetch wallet aliases: {e}"); 1343 exit(2); 1344 } 1345 }; 1346 1347 // Create a prettytable with the new data: 1348 let mut table = Table::new(); 1349 table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); 1350 table.set_titles(row!["Token ID", "Aliases", "Balance"]); 1351 for (token_id, balance) in balmap.iter() { 1352 let aliases = match aliases_map.get(token_id) { 1353 Some(a) => a, 1354 None => "-", 1355 }; 1356 1357 table.add_row(row![ 1358 token_id, 1359 aliases, 1360 encode_base10(*balance, BALANCE_BASE10_DECIMALS) 1361 ]); 1362 } 1363 1364 if table.is_empty() { 1365 println!("No unspent balances found"); 1366 } else { 1367 println!("{table}"); 1368 } 1369 1370 Ok(()) 1371 } 1372 1373 DaoSubcmd::Mint { name } => { 1374 let drk = new_wallet( 1375 blockchain_config.cache_path, 1376 blockchain_config.wallet_path, 1377 blockchain_config.wallet_pass, 1378 Some(blockchain_config.endpoint), 1379 &ex, 1380 args.fun, 1381 ) 1382 .await; 1383 let tx = match drk.dao_mint(&name).await { 1384 Ok(tx) => tx, 1385 Err(e) => { 1386 eprintln!("Failed to mint DAO: {e}"); 1387 exit(2); 1388 } 1389 }; 1390 1391 println!("{}", base64::encode(&serialize_async(&tx).await)); 1392 drk.stop_rpc_client().await 1393 } 1394 1395 DaoSubcmd::ProposeTransfer { 1396 name, 1397 duration, 1398 amount, 1399 token, 1400 recipient, 1401 spend_hook, 1402 user_data, 1403 } => { 1404 let drk = new_wallet( 1405 blockchain_config.cache_path, 1406 blockchain_config.wallet_path, 1407 blockchain_config.wallet_pass, 1408 Some(blockchain_config.endpoint), 1409 &ex, 1410 args.fun, 1411 ) 1412 .await; 1413 1414 if let Err(e) = f64::from_str(&amount) { 1415 eprintln!("Invalid amount: {e}"); 1416 exit(2); 1417 } 1418 1419 let rcpt = match PublicKey::from_str(&recipient) { 1420 Ok(r) => r, 1421 Err(e) => { 1422 eprintln!("Invalid recipient: {e}"); 1423 exit(2); 1424 } 1425 }; 1426 1427 let token_id = match drk.get_token(token).await { 1428 Ok(t) => t, 1429 Err(e) => { 1430 eprintln!("Invalid token alias: {e}"); 1431 exit(2); 1432 } 1433 }; 1434 1435 let spend_hook = match spend_hook { 1436 Some(s) => match FuncId::from_str(&s) { 1437 Ok(s) => Some(s), 1438 Err(e) => { 1439 eprintln!("Invalid spend hook: {e}"); 1440 exit(2); 1441 } 1442 }, 1443 None => None, 1444 }; 1445 1446 let user_data = match user_data { 1447 Some(u) => { 1448 let bytes: [u8; 32] = match bs58::decode(&u).into_vec()?.try_into() { 1449 Ok(b) => b, 1450 Err(e) => { 1451 eprintln!("Invalid user data: {e:?}"); 1452 exit(2); 1453 } 1454 }; 1455 1456 match pallas::Base::from_repr(bytes).into() { 1457 Some(v) => Some(v), 1458 None => { 1459 eprintln!("Invalid user data"); 1460 exit(2); 1461 } 1462 } 1463 } 1464 None => None, 1465 }; 1466 1467 let proposal = match drk 1468 .dao_propose_transfer( 1469 &name, duration, &amount, token_id, rcpt, spend_hook, user_data, 1470 ) 1471 .await 1472 { 1473 Ok(p) => p, 1474 Err(e) => { 1475 eprintln!("Failed to create DAO transfer proposal: {e}"); 1476 exit(2); 1477 } 1478 }; 1479 1480 println!("Generated proposal: {}", proposal.bulla()); 1481 1482 drk.stop_rpc_client().await 1483 } 1484 1485 DaoSubcmd::ProposeGeneric { name, duration, user_data } => { 1486 let drk = new_wallet( 1487 blockchain_config.cache_path, 1488 blockchain_config.wallet_path, 1489 blockchain_config.wallet_pass, 1490 Some(blockchain_config.endpoint), 1491 &ex, 1492 args.fun, 1493 ) 1494 .await; 1495 1496 let user_data = match user_data { 1497 Some(u) => { 1498 let bytes: [u8; 32] = match bs58::decode(&u).into_vec()?.try_into() { 1499 Ok(b) => b, 1500 Err(e) => { 1501 eprintln!("Invalid user data: {e:?}"); 1502 exit(2); 1503 } 1504 }; 1505 1506 match pallas::Base::from_repr(bytes).into() { 1507 Some(v) => Some(v), 1508 None => { 1509 eprintln!("Invalid user data"); 1510 exit(2); 1511 } 1512 } 1513 } 1514 None => None, 1515 }; 1516 1517 let proposal = match drk.dao_propose_generic(&name, duration, user_data).await { 1518 Ok(p) => p, 1519 Err(e) => { 1520 eprintln!("Failed to create DAO generic proposal: {e}"); 1521 exit(2); 1522 } 1523 }; 1524 1525 println!("Generated proposal: {}", proposal.bulla()); 1526 1527 drk.stop_rpc_client().await 1528 } 1529 1530 DaoSubcmd::Proposals { name } => { 1531 let drk = new_wallet( 1532 blockchain_config.cache_path, 1533 blockchain_config.wallet_path, 1534 blockchain_config.wallet_pass, 1535 None, 1536 &ex, 1537 args.fun, 1538 ) 1539 .await; 1540 let proposals = drk.get_dao_proposals(&name).await?; 1541 1542 for (i, proposal) in proposals.iter().enumerate() { 1543 println!("{i}. {}", proposal.bulla()); 1544 } 1545 1546 Ok(()) 1547 } 1548 1549 DaoSubcmd::Proposal { bulla, export, mint_proposal } => { 1550 let bulla = match DaoProposalBulla::from_str(&bulla) { 1551 Ok(b) => b, 1552 Err(e) => { 1553 eprintln!("Invalid proposal bulla: {e}"); 1554 exit(2); 1555 } 1556 }; 1557 1558 let drk = new_wallet( 1559 blockchain_config.cache_path, 1560 blockchain_config.wallet_path, 1561 blockchain_config.wallet_pass, 1562 Some(blockchain_config.endpoint), 1563 &ex, 1564 args.fun, 1565 ) 1566 .await; 1567 let proposal = drk.get_dao_proposal_by_bulla(&bulla).await?; 1568 1569 if export { 1570 // Retrieve the DAO 1571 let dao = drk.get_dao_by_bulla(&proposal.proposal.dao_bulla).await?; 1572 1573 // Encypt the proposal 1574 let enc_note = AeadEncryptedNote::encrypt( 1575 &proposal, 1576 &dao.params.dao.proposals_public_key, 1577 &mut OsRng, 1578 ) 1579 .unwrap(); 1580 1581 // Export it to base64 1582 println!("{}", base64::encode(&serialize_async(&enc_note).await)); 1583 return drk.stop_rpc_client().await 1584 } 1585 1586 if mint_proposal { 1587 // Identify proposal type by its auth calls 1588 for call in &proposal.proposal.auth_calls { 1589 // We only support transfer right now 1590 if call.function_code == DaoFunction::AuthMoneyTransfer as u8 { 1591 let tx = match drk.dao_transfer_proposal_tx(&proposal).await { 1592 Ok(tx) => tx, 1593 Err(e) => { 1594 eprintln!("Failed to create DAO transfer proposal: {e}"); 1595 exit(2); 1596 } 1597 }; 1598 1599 println!("{}", base64::encode(&serialize_async(&tx).await)); 1600 return drk.stop_rpc_client().await 1601 } 1602 } 1603 1604 // If proposal has no auth calls, we consider it a generic one 1605 if proposal.proposal.auth_calls.is_empty() { 1606 let tx = match drk.dao_generic_proposal_tx(&proposal).await { 1607 Ok(tx) => tx, 1608 Err(e) => { 1609 eprintln!("Failed to create DAO generic proposal: {e}"); 1610 exit(2); 1611 } 1612 }; 1613 1614 println!("{}", base64::encode(&serialize_async(&tx).await)); 1615 return drk.stop_rpc_client().await 1616 } 1617 1618 eprintln!("Unsuported DAO proposal"); 1619 exit(2); 1620 } 1621 1622 println!("{proposal}"); 1623 1624 let mut contract_calls = "\nInvoked contracts:\n".to_string(); 1625 for call in proposal.proposal.auth_calls { 1626 contract_calls.push_str(&format!( 1627 "\tContract: {}\n\tFunction: {}\n\tData: ", 1628 call.contract_id, call.function_code 1629 )); 1630 1631 if call.auth_data.is_empty() { 1632 contract_calls.push_str("-\n"); 1633 continue; 1634 } 1635 1636 if call.function_code == DaoFunction::AuthMoneyTransfer as u8 { 1637 // We know that the plaintext data live in the data plaintext vec 1638 if proposal.data.is_none() { 1639 contract_calls.push_str("-\n"); 1640 continue; 1641 } 1642 let coin: CoinAttributes = 1643 deserialize_async(proposal.data.as_ref().unwrap()).await?; 1644 let spend_hook = if coin.spend_hook == FuncId::none() { 1645 "-".to_string() 1646 } else { 1647 format!("{}", coin.spend_hook) 1648 }; 1649 1650 let user_data = if coin.user_data == pallas::Base::ZERO { 1651 "-".to_string() 1652 } else { 1653 format!("{:?}", coin.user_data) 1654 }; 1655 1656 contract_calls.push_str(&format!("\n\t\t{}: {}\n\t\t{}: {} ({})\n\t\t{}: {}\n\t\t{}: {}\n\t\t{}: {}\n\t\t{}: {}\n\n", 1657 "Recipient", 1658 coin.public_key, 1659 "Amount", 1660 coin.value, 1661 encode_base10(coin.value, BALANCE_BASE10_DECIMALS), 1662 "Token", 1663 coin.token_id, 1664 "Spend hook", 1665 spend_hook, 1666 "User data", 1667 user_data, 1668 "Blind", 1669 coin.blind)); 1670 } 1671 } 1672 1673 println!("{contract_calls}"); 1674 1675 let votes = drk.get_dao_proposal_votes(&bulla).await?; 1676 let mut total_yes_vote_value = 0; 1677 let mut total_no_vote_value = 0; 1678 let mut total_all_vote_value = 0; 1679 let mut table = Table::new(); 1680 table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); 1681 table.set_titles(row!["Transaction", "Tokens", "Vote"]); 1682 for vote in votes { 1683 let vote_option = if vote.vote_option { 1684 total_yes_vote_value += vote.all_vote_value; 1685 "Yes" 1686 } else { 1687 total_no_vote_value += vote.all_vote_value; 1688 "No" 1689 }; 1690 total_all_vote_value += vote.all_vote_value; 1691 1692 table.add_row(row![ 1693 vote.tx_hash, 1694 encode_base10(vote.all_vote_value, BALANCE_BASE10_DECIMALS), 1695 vote_option 1696 ]); 1697 } 1698 1699 let outcome = if table.is_empty() { 1700 println!("Votes: No votes found"); 1701 "Unknown" 1702 } else { 1703 println!("Votes:"); 1704 println!("{table}"); 1705 println!( 1706 "Total tokens votes: {}", 1707 encode_base10(total_all_vote_value, BALANCE_BASE10_DECIMALS) 1708 ); 1709 let approval_ratio = 1710 (total_yes_vote_value as f64 * 100.0) / total_all_vote_value as f64; 1711 println!( 1712 "Total tokens Yes votes: {} ({approval_ratio:.2}%)", 1713 encode_base10(total_yes_vote_value, BALANCE_BASE10_DECIMALS) 1714 ); 1715 println!( 1716 "Total tokens No votes: {} ({:.2}%)", 1717 encode_base10(total_no_vote_value, BALANCE_BASE10_DECIMALS), 1718 (total_no_vote_value as f64 * 100.0) / total_all_vote_value as f64 1719 ); 1720 1721 let dao = drk.get_dao_by_bulla(&proposal.proposal.dao_bulla).await?; 1722 if total_all_vote_value >= dao.params.dao.quorum && 1723 approval_ratio >= 1724 (dao.params.dao.approval_ratio_quot / 1725 dao.params.dao.approval_ratio_base) 1726 as f64 1727 { 1728 "Approved" 1729 } else { 1730 "Rejected" 1731 } 1732 }; 1733 1734 if let Some(exec_tx_hash) = proposal.exec_tx_hash { 1735 println!("Proposal was executed on transaction: {exec_tx_hash}"); 1736 return drk.stop_rpc_client().await 1737 } 1738 1739 // Retrieve next block height and current block time target, 1740 // to compute their window. 1741 let next_block_height = drk.get_next_block_height().await?; 1742 let block_target = drk.get_block_target().await?; 1743 let current_window = blockwindow(next_block_height, block_target); 1744 let end_time = proposal.proposal.creation_blockwindow + 1745 proposal.proposal.duration_blockwindows; 1746 let (voting_status, proposal_status_message) = if current_window < end_time { 1747 ("Ongoing", format!("Current proposal outcome: {outcome}")) 1748 } else { 1749 ("Concluded", format!("Proposal outcome: {outcome}")) 1750 }; 1751 println!("Voting status: {voting_status}"); 1752 println!("{proposal_status_message}"); 1753 1754 drk.stop_rpc_client().await 1755 } 1756 1757 DaoSubcmd::ProposalImport => { 1758 let mut buf = String::new(); 1759 stdin().read_to_string(&mut buf)?; 1760 let Some(bytes) = base64::decode(buf.trim()) else { 1761 eprintln!("Failed to decode encrypted proposal data"); 1762 exit(2); 1763 }; 1764 let encrypted_proposal: AeadEncryptedNote = deserialize_async(&bytes).await?; 1765 1766 let drk = new_wallet( 1767 blockchain_config.cache_path, 1768 blockchain_config.wallet_path, 1769 blockchain_config.wallet_pass, 1770 None, 1771 &ex, 1772 args.fun, 1773 ) 1774 .await; 1775 1776 // Retrieve all DAOs to try to decrypt the proposal 1777 let daos = drk.get_daos().await?; 1778 for dao in &daos { 1779 // Check if we have the proposals key 1780 let Some(proposals_secret_key) = dao.params.proposals_secret_key else { 1781 continue 1782 }; 1783 1784 // Try to decrypt the proposal 1785 let Ok(proposal) = 1786 encrypted_proposal.decrypt::<ProposalRecord>(&proposals_secret_key) 1787 else { 1788 continue 1789 }; 1790 1791 let proposal = match drk.get_dao_proposal_by_bulla(&proposal.bulla()).await { 1792 Ok(p) => { 1793 let mut our_proposal = p; 1794 our_proposal.data = proposal.data; 1795 our_proposal 1796 } 1797 Err(_) => proposal, 1798 }; 1799 1800 return drk.put_dao_proposal(&proposal).await 1801 } 1802 1803 eprintln!("Couldn't decrypt the proposal with out DAO keys"); 1804 exit(2); 1805 } 1806 1807 DaoSubcmd::Vote { bulla, vote, vote_weight } => { 1808 let bulla = match DaoProposalBulla::from_str(&bulla) { 1809 Ok(b) => b, 1810 Err(e) => { 1811 eprintln!("Invalid proposal bulla: {e}"); 1812 exit(2); 1813 } 1814 }; 1815 1816 if vote > 1 { 1817 eprintln!("Vote can be either 0 (NO) or 1 (YES)"); 1818 exit(2); 1819 } 1820 let vote = vote != 0; 1821 1822 let weight = match vote_weight { 1823 Some(w) => { 1824 if let Err(e) = f64::from_str(&w) { 1825 eprintln!("Invalid vote weight: {e}"); 1826 exit(2); 1827 } 1828 Some(decode_base10(&w, BALANCE_BASE10_DECIMALS, true)?) 1829 } 1830 None => None, 1831 }; 1832 1833 let drk = new_wallet( 1834 blockchain_config.cache_path, 1835 blockchain_config.wallet_path, 1836 blockchain_config.wallet_pass, 1837 Some(blockchain_config.endpoint), 1838 &ex, 1839 args.fun, 1840 ) 1841 .await; 1842 let tx = match drk.dao_vote(&bulla, vote, weight).await { 1843 Ok(tx) => tx, 1844 Err(e) => { 1845 eprintln!("Failed to create DAO Vote transaction: {e}"); 1846 exit(2); 1847 } 1848 }; 1849 1850 println!("{}", base64::encode(&serialize_async(&tx).await)); 1851 drk.stop_rpc_client().await 1852 } 1853 1854 DaoSubcmd::Exec { bulla, early } => { 1855 let bulla = match DaoProposalBulla::from_str(&bulla) { 1856 Ok(b) => b, 1857 Err(e) => { 1858 eprintln!("Invalid proposal bulla: {e}"); 1859 exit(2); 1860 } 1861 }; 1862 1863 let drk = new_wallet( 1864 blockchain_config.cache_path, 1865 blockchain_config.wallet_path, 1866 blockchain_config.wallet_pass, 1867 Some(blockchain_config.endpoint), 1868 &ex, 1869 args.fun, 1870 ) 1871 .await; 1872 let proposal = drk.get_dao_proposal_by_bulla(&bulla).await?; 1873 1874 // Identify proposal type by its auth calls 1875 for call in &proposal.proposal.auth_calls { 1876 // We only support transfer right now 1877 if call.function_code == DaoFunction::AuthMoneyTransfer as u8 { 1878 let tx = match drk.dao_exec_transfer(&proposal, early).await { 1879 Ok(tx) => tx, 1880 Err(e) => { 1881 eprintln!("Failed to execute DAO transfer proposal: {e}"); 1882 exit(2); 1883 } 1884 }; 1885 1886 println!("{}", base64::encode(&serialize_async(&tx).await)); 1887 return drk.stop_rpc_client().await 1888 } 1889 } 1890 1891 // If proposal has no auth calls, we consider it a generic one 1892 if proposal.proposal.auth_calls.is_empty() { 1893 let tx = match drk.dao_exec_generic(&proposal, early).await { 1894 Ok(tx) => tx, 1895 Err(e) => { 1896 eprintln!("Failed to execute DAO generic proposal: {e}"); 1897 exit(2); 1898 } 1899 }; 1900 1901 println!("{}", base64::encode(&serialize_async(&tx).await)); 1902 return drk.stop_rpc_client().await 1903 } 1904 1905 eprintln!("Unsuported DAO proposal"); 1906 exit(2); 1907 } 1908 1909 DaoSubcmd::SpendHook => { 1910 let spend_hook = 1911 FuncRef { contract_id: *DAO_CONTRACT_ID, func_code: DaoFunction::Exec as u8 } 1912 .to_func_id(); 1913 1914 println!("{spend_hook}"); 1915 1916 Ok(()) 1917 } 1918 }, 1919 1920 Subcmd::AttachFee => { 1921 let mut tx = parse_tx_from_stdin().await?; 1922 1923 let drk = new_wallet( 1924 blockchain_config.cache_path, 1925 blockchain_config.wallet_path, 1926 blockchain_config.wallet_pass, 1927 Some(blockchain_config.endpoint), 1928 &ex, 1929 args.fun, 1930 ) 1931 .await; 1932 if let Err(e) = drk.attach_fee(&mut tx).await { 1933 eprintln!("Failed to attach the fee call to the transaction: {e}"); 1934 exit(2); 1935 }; 1936 1937 println!("{}", base64::encode(&serialize_async(&tx).await)); 1938 1939 drk.stop_rpc_client().await 1940 } 1941 1942 Subcmd::Inspect => { 1943 let tx = parse_tx_from_stdin().await?; 1944 1945 println!("{tx:#?}"); 1946 1947 Ok(()) 1948 } 1949 1950 Subcmd::Broadcast => { 1951 let tx = parse_tx_from_stdin().await?; 1952 1953 let drk = new_wallet( 1954 blockchain_config.cache_path, 1955 blockchain_config.wallet_path, 1956 blockchain_config.wallet_pass, 1957 Some(blockchain_config.endpoint), 1958 &ex, 1959 args.fun, 1960 ) 1961 .await; 1962 1963 if let Err(e) = drk.simulate_tx(&tx).await { 1964 eprintln!("Failed to simulate tx: {e}"); 1965 exit(2); 1966 }; 1967 1968 let mut output = vec![]; 1969 if let Err(e) = drk.mark_tx_spend(&tx, &mut output).await { 1970 print_output(&output); 1971 eprintln!("Failed to mark transaction coins as spent: {e}"); 1972 exit(2); 1973 }; 1974 1975 let txid = match drk.broadcast_tx(&tx, &mut output).await { 1976 Ok(t) => t, 1977 Err(e) => { 1978 print_output(&output); 1979 eprintln!("Failed to broadcast transaction: {e}"); 1980 exit(2); 1981 } 1982 }; 1983 print_output(&output); 1984 1985 println!("Transaction ID: {txid}"); 1986 1987 drk.stop_rpc_client().await 1988 } 1989 1990 Subcmd::Scan { reset } => { 1991 let drk = new_wallet( 1992 blockchain_config.cache_path, 1993 blockchain_config.wallet_path, 1994 blockchain_config.wallet_pass, 1995 Some(blockchain_config.endpoint), 1996 &ex, 1997 args.fun, 1998 ) 1999 .await; 2000 2001 if let Some(height) = reset { 2002 let mut buf = vec![]; 2003 if let Err(e) = drk.reset_to_height(height, &mut buf).await { 2004 print_output(&buf); 2005 eprintln!("Failed during wallet reset: {e}"); 2006 exit(2); 2007 } 2008 print_output(&buf); 2009 } 2010 2011 if let Err(e) = drk.scan_blocks(&mut vec![], None, &true).await { 2012 eprintln!("Failed during scanning: {e}"); 2013 exit(2); 2014 } 2015 println!("Finished scanning blockchain"); 2016 2017 drk.stop_rpc_client().await 2018 } 2019 2020 Subcmd::Explorer { command } => match command { 2021 ExplorerSubcmd::FetchTx { tx_hash, encode } => { 2022 let tx_hash = TransactionHash(*blake3::Hash::from_hex(&tx_hash)?.as_bytes()); 2023 2024 let drk = new_wallet( 2025 blockchain_config.cache_path, 2026 blockchain_config.wallet_path, 2027 blockchain_config.wallet_pass, 2028 Some(blockchain_config.endpoint), 2029 &ex, 2030 args.fun, 2031 ) 2032 .await; 2033 2034 let tx = match drk.get_tx(&tx_hash).await { 2035 Ok(tx) => tx, 2036 Err(e) => { 2037 eprintln!("Failed to fetch transaction: {e}"); 2038 exit(2); 2039 } 2040 }; 2041 2042 let Some(tx) = tx else { 2043 println!("Transaction was not found"); 2044 exit(1); 2045 }; 2046 2047 // Make sure the tx is correct 2048 assert_eq!(tx.hash(), tx_hash); 2049 2050 if encode { 2051 println!("{}", base64::encode(&serialize_async(&tx).await)); 2052 exit(1) 2053 } 2054 2055 println!("Transaction ID: {tx_hash}"); 2056 println!("{tx:?}"); 2057 2058 drk.stop_rpc_client().await 2059 } 2060 2061 ExplorerSubcmd::SimulateTx => { 2062 let tx = parse_tx_from_stdin().await?; 2063 2064 let drk = new_wallet( 2065 blockchain_config.cache_path, 2066 blockchain_config.wallet_path, 2067 blockchain_config.wallet_pass, 2068 Some(blockchain_config.endpoint), 2069 &ex, 2070 args.fun, 2071 ) 2072 .await; 2073 2074 let is_valid = match drk.simulate_tx(&tx).await { 2075 Ok(b) => b, 2076 Err(e) => { 2077 eprintln!("Failed to simulate tx: {e}"); 2078 exit(2); 2079 } 2080 }; 2081 2082 println!("Transaction ID: {}", tx.hash()); 2083 println!("State: {}", if is_valid { "valid" } else { "invalid" }); 2084 2085 drk.stop_rpc_client().await 2086 } 2087 2088 ExplorerSubcmd::TxsHistory { tx_hash, encode } => { 2089 let drk = new_wallet( 2090 blockchain_config.cache_path, 2091 blockchain_config.wallet_path, 2092 blockchain_config.wallet_pass, 2093 None, 2094 &ex, 2095 args.fun, 2096 ) 2097 .await; 2098 2099 if let Some(c) = tx_hash { 2100 let (tx_hash, status, block_height, tx) = drk.get_tx_history_record(&c).await?; 2101 2102 if encode { 2103 println!("{}", base64::encode(&serialize_async(&tx).await)); 2104 exit(1) 2105 } 2106 2107 println!("Transaction ID: {tx_hash}"); 2108 println!("Status: {status}"); 2109 match block_height { 2110 Some(block_height) => println!("Block height: {block_height}"), 2111 None => println!("Block height: -"), 2112 } 2113 println!("{tx:?}"); 2114 2115 return Ok(()) 2116 } 2117 2118 let map = match drk.get_txs_history() { 2119 Ok(m) => m, 2120 Err(e) => { 2121 eprintln!("Failed to retrieve transactions history records: {e}"); 2122 exit(2); 2123 } 2124 }; 2125 2126 // Create a prettytable with the new data: 2127 let mut table = Table::new(); 2128 table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); 2129 table.set_titles(row!["Transaction Hash", "Status", "Block Height"]); 2130 for (txs_hash, status, block_height) in map.iter() { 2131 let block_height = match block_height { 2132 Some(block_height) => block_height.to_string(), 2133 None => String::from("-"), 2134 }; 2135 table.add_row(row![txs_hash, status, block_height]); 2136 } 2137 2138 if table.is_empty() { 2139 println!("No transactions found"); 2140 } else { 2141 println!("{table}"); 2142 } 2143 2144 Ok(()) 2145 } 2146 2147 ExplorerSubcmd::ClearReverted => { 2148 let drk = new_wallet( 2149 blockchain_config.cache_path, 2150 blockchain_config.wallet_path, 2151 blockchain_config.wallet_pass, 2152 None, 2153 &ex, 2154 args.fun, 2155 ) 2156 .await; 2157 2158 let mut output = vec![]; 2159 if let Err(e) = drk.remove_reverted_txs(&mut output) { 2160 print_output(&output); 2161 eprintln!("Failed to remove reverted transactions: {e}"); 2162 exit(2); 2163 }; 2164 print_output(&output); 2165 2166 Ok(()) 2167 } 2168 2169 ExplorerSubcmd::ScannedBlocks { height } => { 2170 let drk = new_wallet( 2171 blockchain_config.cache_path, 2172 blockchain_config.wallet_path, 2173 blockchain_config.wallet_pass, 2174 None, 2175 &ex, 2176 args.fun, 2177 ) 2178 .await; 2179 2180 if let Some(height) = height { 2181 let hash = match drk.get_scanned_block_hash(&height) { 2182 Ok(h) => h, 2183 Err(e) => { 2184 eprintln!("Failed to retrieve scanned block record: {e}"); 2185 exit(2); 2186 } 2187 }; 2188 2189 println!("Height: {height}"); 2190 println!("Hash: {hash}"); 2191 2192 return Ok(()) 2193 } 2194 2195 let map = match drk.get_scanned_block_records() { 2196 Ok(m) => m, 2197 Err(e) => { 2198 eprintln!("Failed to retrieve scanned blocks records: {e}"); 2199 exit(2); 2200 } 2201 }; 2202 2203 // Create a prettytable with the new data: 2204 let mut table = Table::new(); 2205 table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); 2206 table.set_titles(row!["Height", "Hash"]); 2207 for (height, hash) in map.iter() { 2208 table.add_row(row![height, hash]); 2209 } 2210 2211 if table.is_empty() { 2212 println!("No scanned blocks records found"); 2213 } else { 2214 println!("{table}"); 2215 } 2216 2217 Ok(()) 2218 } 2219 }, 2220 2221 Subcmd::Alias { command } => match command { 2222 AliasSubcmd::Add { alias, token } => { 2223 if alias.chars().count() > 5 { 2224 eprintln!("Error: Alias exceeds 5 characters"); 2225 exit(2); 2226 } 2227 2228 let token_id = match TokenId::from_str(token.as_str()) { 2229 Ok(t) => t, 2230 Err(e) => { 2231 eprintln!("Invalid Token ID: {e}"); 2232 exit(2); 2233 } 2234 }; 2235 2236 let drk = new_wallet( 2237 blockchain_config.cache_path, 2238 blockchain_config.wallet_path, 2239 blockchain_config.wallet_pass, 2240 None, 2241 &ex, 2242 args.fun, 2243 ) 2244 .await; 2245 let mut output = vec![]; 2246 if let Err(e) = drk.add_alias(alias, token_id, &mut output).await { 2247 print_output(&output); 2248 eprintln!("Failed to add alias: {e}"); 2249 exit(2); 2250 } 2251 print_output(&output); 2252 2253 Ok(()) 2254 } 2255 2256 AliasSubcmd::Show { alias, token } => { 2257 let token_id = match token { 2258 Some(t) => match TokenId::from_str(t.as_str()) { 2259 Ok(t) => Some(t), 2260 Err(e) => { 2261 eprintln!("Invalid Token ID: {e}"); 2262 exit(2); 2263 } 2264 }, 2265 None => None, 2266 }; 2267 2268 let drk = new_wallet( 2269 blockchain_config.cache_path, 2270 blockchain_config.wallet_path, 2271 blockchain_config.wallet_pass, 2272 None, 2273 &ex, 2274 args.fun, 2275 ) 2276 .await; 2277 let map = drk.get_aliases(alias, token_id).await?; 2278 2279 // Create a prettytable with the new data: 2280 let mut table = Table::new(); 2281 table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); 2282 table.set_titles(row!["Alias", "Token ID"]); 2283 for (alias, token_id) in map.iter() { 2284 table.add_row(row![alias, token_id]); 2285 } 2286 2287 if table.is_empty() { 2288 println!("No aliases found"); 2289 } else { 2290 println!("{table}"); 2291 } 2292 2293 Ok(()) 2294 } 2295 2296 AliasSubcmd::Remove { alias } => { 2297 let drk = new_wallet( 2298 blockchain_config.cache_path, 2299 blockchain_config.wallet_path, 2300 blockchain_config.wallet_pass, 2301 None, 2302 &ex, 2303 args.fun, 2304 ) 2305 .await; 2306 let mut output = vec![]; 2307 if let Err(e) = drk.remove_alias(alias, &mut output).await { 2308 print_output(&output); 2309 eprintln!("Failed to remove alias: {e}"); 2310 exit(2); 2311 } 2312 print_output(&output); 2313 2314 Ok(()) 2315 } 2316 }, 2317 2318 Subcmd::Token { command } => match command { 2319 TokenSubcmd::Import { secret_key, token_blind } => { 2320 let mint_authority = match SecretKey::from_str(&secret_key) { 2321 Ok(ma) => ma, 2322 Err(e) => { 2323 eprintln!("Invalid mint authority: {e}"); 2324 exit(2); 2325 } 2326 }; 2327 2328 let token_blind = match BaseBlind::from_str(&token_blind) { 2329 Ok(tb) => tb, 2330 Err(e) => { 2331 eprintln!("Invalid token blind: {e}"); 2332 exit(2); 2333 } 2334 }; 2335 2336 let drk = new_wallet( 2337 blockchain_config.cache_path, 2338 blockchain_config.wallet_path, 2339 blockchain_config.wallet_pass, 2340 None, 2341 &ex, 2342 args.fun, 2343 ) 2344 .await; 2345 let token_id = drk.import_mint_authority(mint_authority, token_blind).await?; 2346 println!("Successfully imported mint authority for token ID: {token_id}"); 2347 2348 Ok(()) 2349 } 2350 2351 TokenSubcmd::GenerateMint => { 2352 let drk = new_wallet( 2353 blockchain_config.cache_path, 2354 blockchain_config.wallet_path, 2355 blockchain_config.wallet_pass, 2356 None, 2357 &ex, 2358 args.fun, 2359 ) 2360 .await; 2361 let mint_authority = SecretKey::random(&mut OsRng); 2362 let token_blind = BaseBlind::random(&mut OsRng); 2363 let token_id = drk.import_mint_authority(mint_authority, token_blind).await?; 2364 println!("Successfully imported mint authority for token ID: {token_id}"); 2365 2366 Ok(()) 2367 } 2368 2369 TokenSubcmd::List => { 2370 let drk = new_wallet( 2371 blockchain_config.cache_path, 2372 blockchain_config.wallet_path, 2373 blockchain_config.wallet_pass, 2374 None, 2375 &ex, 2376 args.fun, 2377 ) 2378 .await; 2379 let tokens = drk.get_mint_authorities().await?; 2380 let aliases_map = match drk.get_aliases_mapped_by_token().await { 2381 Ok(map) => map, 2382 Err(e) => { 2383 eprintln!("Failed to fetch wallet aliases: {e}"); 2384 exit(2); 2385 } 2386 }; 2387 2388 let mut table = Table::new(); 2389 table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); 2390 table.set_titles(row![ 2391 "Token ID", 2392 "Aliases", 2393 "Mint Authority", 2394 "Token Blind", 2395 "Frozen", 2396 "Freeze Height" 2397 ]); 2398 2399 for (token_id, authority, blind, frozen, freeze_height) in tokens { 2400 let aliases = match aliases_map.get(&token_id.to_string()) { 2401 Some(a) => a, 2402 None => "-", 2403 }; 2404 2405 let freeze_height = match freeze_height { 2406 Some(freeze_height) => freeze_height.to_string(), 2407 None => String::from("-"), 2408 }; 2409 2410 table.add_row(row![token_id, aliases, authority, blind, frozen, freeze_height]); 2411 } 2412 2413 if table.is_empty() { 2414 println!("No tokens found"); 2415 } else { 2416 println!("{table}"); 2417 } 2418 2419 Ok(()) 2420 } 2421 2422 TokenSubcmd::Mint { token, amount, recipient, spend_hook, user_data } => { 2423 let drk = new_wallet( 2424 blockchain_config.cache_path, 2425 blockchain_config.wallet_path, 2426 blockchain_config.wallet_pass, 2427 Some(blockchain_config.endpoint), 2428 &ex, 2429 args.fun, 2430 ) 2431 .await; 2432 2433 if let Err(e) = f64::from_str(&amount) { 2434 eprintln!("Invalid amount: {e}"); 2435 exit(2); 2436 } 2437 2438 let rcpt = match PublicKey::from_str(&recipient) { 2439 Ok(r) => r, 2440 Err(e) => { 2441 eprintln!("Invalid recipient: {e}"); 2442 exit(2); 2443 } 2444 }; 2445 2446 let token_id = match drk.get_token(token).await { 2447 Ok(t) => t, 2448 Err(e) => { 2449 eprintln!("Invalid Token ID: {e}"); 2450 exit(2); 2451 } 2452 }; 2453 2454 let spend_hook = match spend_hook { 2455 Some(s) => match FuncId::from_str(&s) { 2456 Ok(s) => Some(s), 2457 Err(e) => { 2458 eprintln!("Invalid spend hook: {e}"); 2459 exit(2); 2460 } 2461 }, 2462 None => None, 2463 }; 2464 2465 let user_data = match user_data { 2466 Some(u) => { 2467 let bytes: [u8; 32] = match bs58::decode(&u).into_vec()?.try_into() { 2468 Ok(b) => b, 2469 Err(e) => { 2470 eprintln!("Invalid user data: {e:?}"); 2471 exit(2); 2472 } 2473 }; 2474 2475 match pallas::Base::from_repr(bytes).into() { 2476 Some(v) => Some(v), 2477 None => { 2478 eprintln!("Invalid user data"); 2479 exit(2); 2480 } 2481 } 2482 } 2483 None => None, 2484 }; 2485 2486 let tx = match drk.mint_token(&amount, rcpt, token_id, spend_hook, user_data).await 2487 { 2488 Ok(tx) => tx, 2489 Err(e) => { 2490 eprintln!("Failed to create token mint transaction: {e}"); 2491 exit(2); 2492 } 2493 }; 2494 2495 println!("{}", base64::encode(&serialize_async(&tx).await)); 2496 2497 drk.stop_rpc_client().await 2498 } 2499 2500 TokenSubcmd::Freeze { token } => { 2501 let drk = new_wallet( 2502 blockchain_config.cache_path, 2503 blockchain_config.wallet_path, 2504 blockchain_config.wallet_pass, 2505 Some(blockchain_config.endpoint), 2506 &ex, 2507 args.fun, 2508 ) 2509 .await; 2510 let token_id = match drk.get_token(token).await { 2511 Ok(t) => t, 2512 Err(e) => { 2513 eprintln!("Invalid Token ID: {e}"); 2514 exit(2); 2515 } 2516 }; 2517 2518 let tx = match drk.freeze_token(token_id).await { 2519 Ok(tx) => tx, 2520 Err(e) => { 2521 eprintln!("Failed to create token freeze transaction: {e}"); 2522 exit(2); 2523 } 2524 }; 2525 2526 println!("{}", base64::encode(&serialize_async(&tx).await)); 2527 2528 drk.stop_rpc_client().await 2529 } 2530 }, 2531 2532 Subcmd::Contract { command } => match command { 2533 ContractSubcmd::GenerateDeploy => { 2534 let drk = new_wallet( 2535 blockchain_config.cache_path, 2536 blockchain_config.wallet_path, 2537 blockchain_config.wallet_pass, 2538 None, 2539 &ex, 2540 args.fun, 2541 ) 2542 .await; 2543 2544 let mut output = vec![]; 2545 if let Err(e) = drk.deploy_auth_keygen(&mut output).await { 2546 print_output(&output); 2547 eprintln!("Error creating deploy auth keypair: {e}"); 2548 exit(2); 2549 } 2550 print_output(&output); 2551 2552 Ok(()) 2553 } 2554 2555 ContractSubcmd::List { contract_id } => { 2556 let drk = new_wallet( 2557 blockchain_config.cache_path, 2558 blockchain_config.wallet_path, 2559 blockchain_config.wallet_pass, 2560 None, 2561 &ex, 2562 args.fun, 2563 ) 2564 .await; 2565 2566 if let Some(contract_id) = contract_id { 2567 let contract_id = match ContractId::from_str(&contract_id) { 2568 Ok(d) => d, 2569 Err(e) => { 2570 eprintln!("Invalid contract id: {e}"); 2571 exit(2); 2572 } 2573 }; 2574 2575 let history = drk.get_deploy_auth_history(&contract_id).await?; 2576 2577 let mut table = Table::new(); 2578 table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); 2579 table.set_titles(row!["Transaction Hash", "Type", "Block Height"]); 2580 2581 for (tx_hash, tx_type, block_height) in history { 2582 table.add_row(row![tx_hash, tx_type, block_height]); 2583 } 2584 2585 if table.is_empty() { 2586 println!("No history records found"); 2587 } else { 2588 println!("{table}"); 2589 } 2590 2591 return Ok(()) 2592 } 2593 2594 let auths = drk.list_deploy_auth().await?; 2595 2596 let mut table = Table::new(); 2597 table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); 2598 table.set_titles(row!["Contract ID", "Secret Key", "Locked", "Lock Height"]); 2599 2600 for (contract_id, secret_key, is_locked, lock_height) in auths { 2601 let lock_height = match lock_height { 2602 Some(lock_height) => lock_height.to_string(), 2603 None => String::from("-"), 2604 }; 2605 table.add_row(row![contract_id, secret_key, is_locked, lock_height]); 2606 } 2607 2608 if table.is_empty() { 2609 println!("No deploy authorities found"); 2610 } else { 2611 println!("{table}"); 2612 } 2613 2614 Ok(()) 2615 } 2616 2617 ContractSubcmd::ExportData { tx_hash } => { 2618 let drk = new_wallet( 2619 blockchain_config.cache_path, 2620 blockchain_config.wallet_path, 2621 blockchain_config.wallet_pass, 2622 None, 2623 &ex, 2624 args.fun, 2625 ) 2626 .await; 2627 2628 let pair = drk.get_deploy_history_record_data(&tx_hash).await?; 2629 2630 println!("{}", base64::encode(&serialize_async(&pair).await)); 2631 2632 Ok(()) 2633 } 2634 2635 ContractSubcmd::Deploy { deploy_auth, wasm_path, deploy_ix } => { 2636 // Parse the deployment authority contract id 2637 let deploy_auth = match ContractId::from_str(&deploy_auth) { 2638 Ok(d) => d, 2639 Err(e) => { 2640 eprintln!("Invalid deploy authority: {e}"); 2641 exit(2); 2642 } 2643 }; 2644 2645 // Read the wasm bincode and deploy instruction 2646 let wasm_bin = smol::fs::read(expand_path(&wasm_path)?).await?; 2647 let deploy_ix = match deploy_ix { 2648 Some(p) => smol::fs::read(expand_path(&p)?).await?, 2649 None => vec![], 2650 }; 2651 2652 let drk = new_wallet( 2653 blockchain_config.cache_path, 2654 blockchain_config.wallet_path, 2655 blockchain_config.wallet_pass, 2656 Some(blockchain_config.endpoint), 2657 &ex, 2658 args.fun, 2659 ) 2660 .await; 2661 2662 let tx = match drk.deploy_contract(&deploy_auth, wasm_bin, deploy_ix).await { 2663 Ok(tx) => tx, 2664 Err(e) => { 2665 eprintln!("Error creating contract deployment tx: {e}"); 2666 exit(2); 2667 } 2668 }; 2669 2670 println!("{}", base64::encode(&serialize_async(&tx).await)); 2671 2672 drk.stop_rpc_client().await 2673 } 2674 2675 ContractSubcmd::Lock { deploy_auth } => { 2676 // Parse the deployment authority contract id 2677 let deploy_auth = match ContractId::from_str(&deploy_auth) { 2678 Ok(d) => d, 2679 Err(e) => { 2680 eprintln!("Invalid deploy authority: {e}"); 2681 exit(2); 2682 } 2683 }; 2684 2685 let drk = new_wallet( 2686 blockchain_config.cache_path, 2687 blockchain_config.wallet_path, 2688 blockchain_config.wallet_pass, 2689 Some(blockchain_config.endpoint), 2690 &ex, 2691 args.fun, 2692 ) 2693 .await; 2694 2695 let tx = match drk.lock_contract(&deploy_auth).await { 2696 Ok(tx) => tx, 2697 Err(e) => { 2698 eprintln!("Error creating contract lock tx: {e}"); 2699 exit(2); 2700 } 2701 }; 2702 2703 println!("{}", base64::encode(&serialize_async(&tx).await)); 2704 2705 drk.stop_rpc_client().await 2706 } 2707 }, 2708 } 2709 }