/* This file is part of DarkFi (https://dark.fi) * * Copyright (C) 2020-2026 Dyne.org foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ use std::{ io::{stdin, Read}, process::exit, str::FromStr, }; use prettytable::{format, row, Table}; use rand::rngs::OsRng; use smol::{channel::unbounded, fs::read_to_string, stream::StreamExt}; use structopt_toml::{serde::Deserialize, structopt::StructOpt, StructOptToml}; use tracing::info; use tracing_appender::non_blocking; use url::Url; use darkfi::{ async_daemonize, cli_desc, system::ExecutorPtr, util::{ encoding::base64, logger::{set_terminal_writer, ChannelWriter}, parse::{decode_base10, encode_base10}, path::{expand_path, get_config_path}, }, zk::halo2::Field, Error, Result, }; use darkfi_dao_contract::{blockwindow, model::DaoProposalBulla, DaoFunction}; use darkfi_money_contract::model::{Coin, CoinAttributes, TokenId}; use darkfi_sdk::{ crypto::{ keypair::{Address, Keypair, Network, SecretKey, StandardAddress}, note::AeadEncryptedNote, BaseBlind, ContractId, FuncId, FuncRef, DAO_CONTRACT_ID, }, pasta::{group::ff::PrimeField, pallas}, tx::TransactionHash, }; use darkfi_serial::{deserialize_async, serialize_async}; use drk::{ cli_util::{ display_mining_config, generate_completions, kaching, parse_calls_from_stdin, parse_mining_config_from_stdin, parse_token_pair, parse_tree, parse_tx_from_stdin, parse_value_pair, print_output, tx_from_calls_mapped, }, common::*, dao::{DaoParams, ProposalRecord}, interactive::interactive, money::BALANCE_BASE10_DECIMALS, swap::PartialSwapData, Drk, }; const CONFIG_FILE: &str = "drk_config.toml"; const CONFIG_FILE_CONTENTS: &str = include_str!("../drk_config.toml"); // Dev Note: when adding/modifying args here, // don't forget to update cli_util::generate_completions() // and interactive::help(). #[derive(Clone, Debug, Deserialize, StructOpt, StructOptToml)] #[serde(default)] #[structopt(name = "drk", about = cli_desc!())] struct Args { #[structopt(short, long)] /// Configuration file to use config: Option, #[structopt(short, long, default_value = "testnet")] /// Blockchain network to use network: String, #[structopt(subcommand)] /// Sub command to execute command: Subcmd, #[structopt(short, long)] /// Flag indicating whether you want some fun in your life fun: bool, #[structopt(short, long)] /// Set log file to ouput into log: Option, #[structopt(short, parse(from_occurrences))] /// Increase verbosity (-vvv supported) verbose: u8, } // Dev Note: when adding/modifying commands here, // don't forget to update cli_util::generate_completions() #[derive(Clone, Debug, Deserialize, StructOpt)] enum Subcmd { /// Enter Drk interactive shell Interactive, /// Fun Kaching, /// Send a ping request to the darkfid RPC endpoint Ping, /// Generate a SHELL completion script and print to stdout Completions { /// The Shell you want to generate script for shell: String, }, /// Wallet operations Wallet { #[structopt(subcommand)] /// Sub command to execute command: WalletSubcmd, }, /// Read a transaction from stdin and mark its input coins as spent Spend, /// Unspend a coin Unspend { /// base64-encoded coin to mark as unspent coin: String, }, /// Create a payment transaction Transfer { /// Amount to send amount: String, /// Token ID to send token: String, /// Recipient address recipient: String, /// Optional contract spend hook to use spend_hook: Option, /// Optional user data to use user_data: Option, #[structopt(long)] /// Split the output coin into two equal halves half_split: bool, }, /// OTC atomic swap Otc { #[structopt(subcommand)] /// Sub command to execute command: OtcSubcmd, }, /// DAO functionalities Dao { #[structopt(subcommand)] /// Sub command to execute command: DaoSubcmd, }, /// Attach the fee call to a transaction given from stdin AttachFee, /// Create a transaction from newline-separated calls from stdin TxFromCalls { /// Optional parent/children dependency map for the calls calls_map: Option, }, /// Inspect a transaction from stdin Inspect, /// Read a transaction from stdin and broadcast it Broadcast, /// Scan the blockchain and parse relevant transactions Scan { #[structopt(long)] /// Reset wallet state to provided block height and start scanning reset: Option, }, /// Explorer related subcommands Explorer { #[structopt(subcommand)] /// Sub command to execute command: ExplorerSubcmd, }, /// Manage Token aliases Alias { #[structopt(subcommand)] /// Sub command to execute command: AliasSubcmd, }, /// Token functionalities Token { #[structopt(subcommand)] /// Sub command to execute command: TokenSubcmd, }, /// Contract functionalities Contract { #[structopt(subcommand)] /// Sub command to execute command: ContractSubcmd, }, } #[derive(Clone, Debug, Deserialize, StructOpt)] enum WalletSubcmd { /// Initialize wallet database Initialize, /// Generate a new keypair in the wallet Keygen, /// Query the wallet for known balances Balance, /// Get the default address in the wallet Address, /// Print all the addresses in the wallet Addresses, /// Set the default address in the wallet DefaultAddress { /// Identifier of the address index: usize, }, /// Print all the secret keys from the wallet Secrets, /// Import secret keys from stdin into the wallet, separated by newlines ImportSecrets, /// Print the Merkle tree in the wallet Tree, /// Print all the coins in the wallet Coins, /// Print a wallet address mining configuration MiningConfig { /// Identifier of the address index: usize, /// Optional contract spend hook to use spend_hook: Option, /// Optional user data to use user_data: Option, }, } #[derive(Clone, Debug, Deserialize, StructOpt)] enum OtcSubcmd { /// Initialize the first half of the atomic swap Init { /// Value pair to send:recv (11.55:99.42) #[structopt(short, long)] value_pair: String, /// Token pair to send:recv (f00:b4r) #[structopt(short, long)] token_pair: String, }, /// Build entire swap tx given the first half from stdin Join, /// Inspect a swap half or the full swap tx from stdin Inspect, /// Sign a swap transaction given from stdin Sign, } #[derive(Clone, Debug, Deserialize, StructOpt)] enum DaoSubcmd { /// Create DAO parameters Create { /// The minimum amount of governance tokens needed to open a proposal for this DAO proposer_limit: String, /// Minimal threshold of participating total tokens needed for a proposal to pass quorum: String, /// Minimal threshold of participating total tokens needed for a proposal to /// be considered as strongly supported, enabling early execution. /// Must be greater or equal to normal quorum. early_exec_quorum: String, /// The ratio of winning votes/total votes needed for a proposal to pass (2 decimals) approval_ratio: f64, /// DAO's governance token ID gov_token_id: String, }, /// View DAO data from stdin View, /// Import DAO data from stdin Import { /// Name identifier for the DAO name: String, }, /// List imported DAOs (or info about a specific one) List { /// Name identifier for the DAO (optional) name: Option, }, /// Show the balance of a DAO Balance { /// Name identifier for the DAO name: String, }, /// Mint an imported DAO on-chain Mint { /// Name identifier for the DAO name: String, }, /// Create a transfer proposal for a DAO ProposeTransfer { /// Name identifier for the DAO name: String, /// Duration of the proposal, in block windows duration: u64, /// Amount to send amount: String, /// Token ID to send token: String, /// Recipient address recipient: String, /// Optional contract spend hook to use spend_hook: Option, /// Optional user data to use user_data: Option, }, /// Create a generic proposal for a DAO ProposeGeneric { /// Name identifier for the DAO name: String, /// Duration of the proposal, in block windows duration: u64, /// Optional user data to use user_data: Option, }, /// List DAO proposals Proposals { /// Name identifier for the DAO name: String, }, /// View a DAO proposal data Proposal { /// Bulla identifier for the proposal bulla: String, #[structopt(long)] /// Encrypt the proposal and encode it to base64 export: bool, #[structopt(long)] /// Create the proposal transaction mint_proposal: bool, }, /// Import a base64 encoded and encrypted proposal from stdin ProposalImport, /// Vote on a given proposal Vote { /// Bulla identifier for the proposal bulla: String, /// Vote (0 for NO, 1 for YES) vote: u8, /// Optional vote weight (amount of governance tokens) vote_weight: Option, }, /// Execute a DAO proposal Exec { /// Bulla identifier for the proposal bulla: String, #[structopt(long)] /// Execute the proposal early early: bool, }, /// Print the DAO contract base64-encoded spend hook SpendHook, /// Print a DAO mining configuration MiningConfig { /// Name identifier for the DAO name: String, }, } #[derive(Clone, Debug, Deserialize, StructOpt)] enum ExplorerSubcmd { /// Fetch a blockchain transaction by hash FetchTx { /// Transaction hash tx_hash: String, #[structopt(long)] /// Encode transaction to base64 encode: bool, }, /// Read a transaction from stdin and simulate it SimulateTx, /// Fetch broadcasted transactions history TxsHistory { /// Fetch specific history record (optional) tx_hash: Option, #[structopt(long)] /// Encode specific history record transaction to base64 encode: bool, }, /// Remove reverted transactions from history ClearReverted, /// Fetch scanned blocks records ScannedBlocks { /// Fetch specific height record (optional) height: Option, }, /// Read a mining configuration from stdin and display its parts MiningConfig, } #[derive(Clone, Debug, Deserialize, StructOpt)] enum AliasSubcmd { /// Create a Token alias Add { /// Token alias alias: String, /// Token to create alias for token: String, }, /// Print alias info of optional arguments. /// If no argument is provided, list all the aliases in the wallet. Show { /// Token alias to search for #[structopt(short, long)] alias: Option, /// Token to search alias for #[structopt(short, long)] token: Option, }, /// Remove a Token alias Remove { /// Token alias to remove alias: String, }, } #[derive(Clone, Debug, Deserialize, StructOpt)] enum TokenSubcmd { /// Import a mint authority Import { /// Mint authority secret key secret_key: String, /// Mint authority token blind token_blind: String, }, /// Generate a new mint authority GenerateMint, /// List token IDs with available mint authorities List, /// Mint tokens Mint { /// Token ID to mint token: String, /// Amount to mint amount: String, /// Recipient of the minted tokens recipient: String, /// Optional contract spend hook to use spend_hook: Option, /// Optional user data to use user_data: Option, }, /// Freeze a token mint Freeze { /// Token ID to freeze token: String, }, } #[derive(Clone, Debug, Deserialize, StructOpt)] enum ContractSubcmd { /// Generate a new deploy authority GenerateDeploy, /// List deploy authorities in the wallet (or a specific one) List { /// Contract ID (optional) contract_id: Option, }, /// Export a contract history record wasm bincode and deployment instruction, encoded to base64 ExportData { /// Record transaction hash tx_hash: String, }, /// Deploy a smart contract Deploy { /// Contract ID (deploy authority) deploy_auth: String, /// Path to contract wasm bincode wasm_path: String, /// Optional path to serialized deploy instruction deploy_ix: Option, }, /// Lock a smart contract Lock { /// Contract ID (deploy authority) deploy_auth: String, }, } /// Defines a blockchain network configuration. /// Default values correspond to a local network. #[derive(Clone, Debug, serde::Deserialize, structopt::StructOpt, structopt_toml::StructOptToml)] #[structopt()] struct BlockchainNetwork { #[structopt(long, default_value = "~/.local/share/darkfi/drk/localnet/cache")] /// Path to blockchain cache database cache_path: String, #[structopt(long, default_value = "~/.local/share/darkfi/drk/localnet/wallet.db")] /// Path to wallet database wallet_path: String, #[structopt(long, default_value = "changeme")] /// Password for the wallet database wallet_pass: String, #[structopt(short, long, default_value = "tcp://127.0.0.1:28345")] /// darkfid JSON-RPC endpoint endpoint: Url, #[structopt(long, default_value = "~/.local/share/darkfi/drk/localnet/history.txt")] /// Path to interactive shell history file history_path: String, } /// Auxiliary function to parse darkfid configuration file and extract requested /// blockchain network config. async fn parse_blockchain_config( config: Option, network: &str, ) -> Result<(Network, BlockchainNetwork)> { // Grab network let used_net = match network { "mainnet" | "localnet" => Network::Mainnet, "testnet" => Network::Testnet, _ => return Err(Error::ParseFailed("Invalid blockchain network")), }; // Grab config path let config_path = get_config_path(config, CONFIG_FILE)?; // Parse TOML file contents let contents = read_to_string(&config_path).await?; let contents: toml::Value = match toml::from_str(&contents) { Ok(v) => v, Err(e) => { eprintln!("Failed parsing TOML config: {e}"); return Err(Error::ParseFailed("Failed parsing TOML config")) } }; // Grab requested network config let Some(table) = contents.as_table() else { return Err(Error::ParseFailed("TOML not a map")) }; let Some(network_configs) = table.get("network_config") else { return Err(Error::ParseFailed("TOML does not contain network configurations")) }; let Some(network_configs) = network_configs.as_table() else { return Err(Error::ParseFailed("`network_config` not a map")) }; let Some(network_config) = network_configs.get(network) else { return Err(Error::ParseFailed("TOML does not contain requested network configuration")) }; let network_config = toml::to_string(&network_config).unwrap(); let network_config = match BlockchainNetwork::from_iter_with_toml::>(&network_config, vec![]) { Ok(v) => v, Err(e) => { eprintln!("Failed parsing requested network configuration: {e}"); return Err(Error::ParseFailed("Failed parsing requested network configuration")) } }; Ok((used_net, network_config)) } /// Auxiliary function to create a `Drk` wallet for provided configuration. async fn new_wallet( network: Network, cache_path: String, wallet_path: String, wallet_pass: String, endpoint: Option, ex: &ExecutorPtr, fun: bool, ) -> Drk { // Script kiddies protection if wallet_pass == "changeme" { eprintln!("Please don't use default wallet password..."); exit(2); } match Drk::new(network, cache_path, wallet_path, wallet_pass, endpoint, ex, fun).await { Ok(wallet) => wallet, Err(e) => { eprintln!("Error initializing wallet: {e}"); exit(2); } } } async_daemonize!(realmain); async fn realmain(args: Args, ex: ExecutorPtr) -> Result<()> { // Grab blockchain network configuration let (network, blockchain_config) = match args.network.as_str() { "localnet" => parse_blockchain_config(args.config, "localnet").await?, "testnet" => parse_blockchain_config(args.config, "testnet").await?, "mainnet" => parse_blockchain_config(args.config, "mainnet").await?, _ => { eprintln!("Unsupported chain `{}`", args.network); return Err(Error::UnsupportedChain) } }; match args.command { Subcmd::Interactive => { // Create an unbounded smol channel, so we can have a // printing queue the background logger and tasks can // submit messages to so the shell prints them. let (shell_sender, shell_receiver) = unbounded(); // Set the logging writer let (non_blocking, _guard) = non_blocking(ChannelWriter { sender: shell_sender.clone() }); set_terminal_writer(args.verbose, non_blocking)?; let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, Some(blockchain_config.endpoint.clone()), &ex, args.fun, ) .await .into_ptr(); interactive( &drk, &blockchain_config.endpoint, &blockchain_config.history_path, &shell_sender, &shell_receiver, &ex, ) .await; drk.read().await.stop_rpc_client().await?; Ok(()) } Subcmd::Kaching => { if !args.fun { println!("Apparently you don't like fun..."); return Ok(()) } kaching().await; Ok(()) } Subcmd::Ping => { let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, Some(blockchain_config.endpoint), &ex, args.fun, ) .await; let mut output = vec![]; if let Err(e) = drk.ping(&mut output).await { print_output(&output); return Err(e) }; print_output(&output); drk.stop_rpc_client().await } Subcmd::Completions { shell } => { println!("{}", generate_completions(&shell)?); Ok(()) } Subcmd::Wallet { command } => { let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, None, &ex, args.fun, ) .await; match command { WalletSubcmd::Initialize => { if let Err(e) = drk.initialize_wallet().await { eprintln!("Error initializing wallet: {e}"); exit(2); } let mut output = vec![]; if let Err(e) = drk.initialize_money(&mut output).await { print_output(&output); eprintln!("Failed to initialize Money: {e}"); exit(2); } print_output(&output); if let Err(e) = drk.initialize_dao().await { eprintln!("Failed to initialize DAO: {e}"); exit(2); } if let Err(e) = drk.initialize_deployooor() { eprintln!("Failed to initialize Deployooor: {e}"); exit(2); } } WalletSubcmd::Keygen => { let mut output = vec![]; if let Err(e) = drk.money_keygen(&mut output).await { print_output(&output); eprintln!("Failed to generate keypair: {e}"); exit(2); } print_output(&output); } WalletSubcmd::Balance => { let balmap = drk.money_balance().await?; let aliases_map = drk.get_aliases_mapped_by_token().await?; // Create a prettytable with the new data: let mut table = Table::new(); table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); table.set_titles(row!["Token ID", "Aliases", "Balance"]); for (token_id, balance) in balmap.iter() { let aliases = match aliases_map.get(token_id) { Some(a) => a, None => "-", }; table.add_row(row![ token_id, aliases, encode_base10(*balance, BALANCE_BASE10_DECIMALS) ]); } if table.is_empty() { println!("No unspent balances found"); } else { println!("{table}"); } } WalletSubcmd::Address => match drk.default_address().await { Ok(public_key) => { let address: Address = StandardAddress::from_public(drk.network, public_key).into(); println!("{address}"); } Err(e) => { eprintln!("Failed to fetch default address: {e}"); exit(2); } }, WalletSubcmd::Addresses => { let addresses = drk.addresses().await?; let table = prettytable_addrs(drk.network, &addresses); if table.is_empty() { println!("No addresses found"); } else { println!("{table}"); } } WalletSubcmd::DefaultAddress { index } => { if let Err(e) = drk.set_default_address(index) { eprintln!("Failed to set default address: {e}"); exit(2); } } WalletSubcmd::Secrets => { for secret in drk.get_money_secrets().await? { println!("{secret}"); } } WalletSubcmd::ImportSecrets => { let mut secrets = vec![]; let lines = stdin().lines(); for (i, line) in lines.enumerate() { if let Ok(line) = line { let bytes = bs58::decode(&line.trim()).into_vec()?; let Ok(secret) = deserialize_async(&bytes).await else { println!("Warning: Failed to deserialize secret on line {i}"); continue }; secrets.push(secret); } } let mut output = vec![]; let pubkeys = match drk.import_money_secrets(secrets, &mut output).await { Ok(p) => { print_output(&output); p } Err(e) => { print_output(&output); eprintln!("Failed to import secret keys into wallet: {e}"); exit(2); } }; for key in pubkeys { println!("{key}"); } } WalletSubcmd::Tree => { println!("{:#?}", drk.get_money_tree().await?); } WalletSubcmd::Coins => { let coins = drk.get_coins(true).await?; if coins.is_empty() { return Ok(()) } let aliases_map = drk.get_aliases_mapped_by_token().await?; let table = prettytable_coins(&coins, &aliases_map); println!("{table}"); } WalletSubcmd::MiningConfig { index, spend_hook, user_data } => { let spend_hook = match spend_hook { Some(s) => match FuncId::from_str(&s) { Ok(s) => Some(s), Err(e) => { eprintln!("Invalid spend hook: {e}"); exit(2); } }, None => None, }; let user_data = match user_data { Some(u) => { let bytes: [u8; 32] = match bs58::decode(&u).into_vec()?.try_into() { Ok(b) => b, Err(e) => { eprintln!("Invalid user data: {e:?}"); exit(2); } }; match pallas::Base::from_repr(bytes).into() { Some(v) => Some(v), None => { eprintln!("Invalid user data"); exit(2); } } } None => None, }; let mut output = vec![]; if let Err(e) = drk.mining_config(index, spend_hook, user_data, &mut output).await { print_output(&output); eprintln!("Failed to generate wallet mining configuration: {e}"); exit(2); } print_output(&output); } } Ok(()) } Subcmd::Spend => { let tx = parse_tx_from_stdin().await?; let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, None, &ex, args.fun, ) .await; let mut output = vec![]; if let Err(e) = drk.mark_tx_spend(&tx, &mut output).await { print_output(&output); eprintln!("Failed to mark transaction coins as spent: {e}"); exit(2); }; print_output(&output); Ok(()) } Subcmd::Unspend { coin } => { let bytes: [u8; 32] = match bs58::decode(&coin).into_vec()?.try_into() { Ok(b) => b, Err(e) => { eprintln!("Invalid coin: {e:?}"); exit(2); } }; let elem: pallas::Base = match pallas::Base::from_repr(bytes).into() { Some(v) => v, None => { eprintln!("Invalid coin"); exit(2); } }; let coin = Coin::from(elem); let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, None, &ex, args.fun, ) .await; if let Err(e) = drk.unspend_coin(&coin).await { eprintln!("Failed to mark coin as unspent: {e}"); exit(2); } Ok(()) } Subcmd::Transfer { amount, token, recipient, spend_hook, user_data, half_split } => { let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, Some(blockchain_config.endpoint), &ex, args.fun, ) .await; if let Err(e) = f64::from_str(&amount) { eprintln!("Invalid amount: {e}"); exit(2); } let rcpt = match Address::from_str(&recipient) { Ok(r) => r, Err(e) => { eprintln!("Invalid recipient: {e}"); exit(2); } }; if rcpt.network() != drk.network { eprintln!("Recipient address prefix mismatch"); exit(2); } let token_id = match drk.get_token(token).await { Ok(t) => t, Err(e) => { eprintln!("Invalid token alias: {e}"); exit(2); } }; let spend_hook = match spend_hook { Some(s) => match FuncId::from_str(&s) { Ok(s) => Some(s), Err(e) => { eprintln!("Invalid spend hook: {e}"); exit(2); } }, None => None, }; let user_data = match user_data { Some(u) => { let bytes: [u8; 32] = match bs58::decode(&u).into_vec()?.try_into() { Ok(b) => b, Err(e) => { eprintln!("Invalid user data: {e:?}"); exit(2); } }; match pallas::Base::from_repr(bytes).into() { Some(v) => Some(v), None => { eprintln!("Invalid user data"); exit(2); } } } None => None, }; let tx = match drk .transfer(&amount, token_id, *rcpt.public_key(), spend_hook, user_data, half_split) .await { Ok(t) => t, Err(e) => { eprintln!("Failed to create payment transaction: {e}"); exit(2); } }; println!("{}", base64::encode(&serialize_async(&tx).await)); drk.stop_rpc_client().await } Subcmd::Otc { command } => match command { OtcSubcmd::Init { value_pair, token_pair } => { let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, Some(blockchain_config.endpoint), &ex, args.fun, ) .await; let value_pair = parse_value_pair(&value_pair)?; let token_pair = parse_token_pair(&drk, &token_pair).await?; let half = match drk.init_swap(value_pair, token_pair, None, None, None).await { Ok(h) => h, Err(e) => { eprintln!("Failed to create swap transaction half: {e}"); exit(2); } }; println!("{}", base64::encode(&serialize_async(&half).await)); drk.stop_rpc_client().await } OtcSubcmd::Join => { let mut buf = String::new(); stdin().read_to_string(&mut buf)?; let Some(bytes) = base64::decode(buf.trim()) else { eprintln!("Failed to decode partial swap data"); exit(2); }; let partial: PartialSwapData = deserialize_async(&bytes).await?; let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, Some(blockchain_config.endpoint), &ex, args.fun, ) .await; let tx = match drk.join_swap(partial, None, None, None).await { Ok(tx) => tx, Err(e) => { eprintln!("Failed to create a join swap transaction: {e}"); exit(2); } }; println!("{}", base64::encode(&serialize_async(&tx).await)); drk.stop_rpc_client().await } OtcSubcmd::Inspect => { let mut buf = String::new(); stdin().read_to_string(&mut buf)?; let Some(bytes) = base64::decode(buf.trim()) else { eprintln!("Failed to decode swap transaction"); exit(2); }; let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, None, &ex, args.fun, ) .await; let mut output = vec![]; if let Err(e) = drk.inspect_swap(bytes, &mut output).await { print_output(&output); eprintln!("Failed to inspect swap: {e}"); exit(2); }; print_output(&output); Ok(()) } OtcSubcmd::Sign => { let mut tx = parse_tx_from_stdin().await?; let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, None, &ex, args.fun, ) .await; if let Err(e) = drk.sign_swap(&mut tx).await { eprintln!("Failed to sign joined swap transaction: {e}"); exit(2); }; println!("{}", base64::encode(&serialize_async(&tx).await)); Ok(()) } }, Subcmd::Dao { command } => match command { DaoSubcmd::Create { proposer_limit, quorum, early_exec_quorum, approval_ratio, gov_token_id, } => { if let Err(e) = f64::from_str(&proposer_limit) { eprintln!("Invalid proposer limit: {e}"); exit(2); } if let Err(e) = f64::from_str(&quorum) { eprintln!("Invalid quorum: {e}"); exit(2); } if let Err(e) = f64::from_str(&early_exec_quorum) { eprintln!("Invalid early exec quorum: {e}"); exit(2); } let proposer_limit = decode_base10(&proposer_limit, BALANCE_BASE10_DECIMALS, true)?; let quorum = decode_base10(&quorum, BALANCE_BASE10_DECIMALS, true)?; let early_exec_quorum = decode_base10(&early_exec_quorum, BALANCE_BASE10_DECIMALS, true)?; if approval_ratio > 1.0 { eprintln!("Error: Approval ratio cannot be >1.0"); exit(2); } let approval_ratio_base = 100_u64; let approval_ratio_quot = (approval_ratio * approval_ratio_base as f64) as u64; let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, None, &ex, args.fun, ) .await; let gov_token_id = match drk.get_token(gov_token_id).await { Ok(g) => g, Err(e) => { eprintln!("Invalid Token ID: {e}"); exit(2); } }; let notes_keypair = Keypair::random(&mut OsRng); let proposer_keypair = Keypair::random(&mut OsRng); let proposals_keypair = Keypair::random(&mut OsRng); let votes_keypair = Keypair::random(&mut OsRng); let exec_keypair = Keypair::random(&mut OsRng); let early_exec_keypair = Keypair::random(&mut OsRng); let bulla_blind = BaseBlind::random(&mut OsRng); let params = DaoParams::new( proposer_limit, quorum, early_exec_quorum, approval_ratio_base, approval_ratio_quot, gov_token_id, Some(notes_keypair.secret), notes_keypair.public, Some(proposer_keypair.secret), proposer_keypair.public, Some(proposals_keypair.secret), proposals_keypair.public, Some(votes_keypair.secret), votes_keypair.public, Some(exec_keypair.secret), exec_keypair.public, Some(early_exec_keypair.secret), early_exec_keypair.public, bulla_blind, ); println!("{}", params.toml_str()); Ok(()) } DaoSubcmd::View => { let mut buf = String::new(); stdin().read_to_string(&mut buf)?; let params = DaoParams::from_toml_str(&buf)?; println!("{params}"); Ok(()) } DaoSubcmd::Import { name } => { let mut buf = String::new(); stdin().read_to_string(&mut buf)?; let params = DaoParams::from_toml_str(&buf)?; let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, None, &ex, args.fun, ) .await; let mut output = vec![]; if let Err(e) = drk.import_dao(&name, ¶ms, &mut output).await { print_output(&output); eprintln!("Failed to import DAO: {e}"); exit(2); } print_output(&output); Ok(()) } DaoSubcmd::List { name } => { let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, None, &ex, args.fun, ) .await; let mut output = vec![]; if let Err(e) = drk.dao_list(&name, &mut output).await { print_output(&output); eprintln!("Failed to list DAO: {e}"); exit(2); } print_output(&output); Ok(()) } DaoSubcmd::Balance { name } => { let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, None, &ex, args.fun, ) .await; let balmap = match drk.dao_balance(&name).await { Ok(b) => b, Err(e) => { eprintln!("Failed to fetch DAO balance: {e}"); exit(2); } }; let aliases_map = match drk.get_aliases_mapped_by_token().await { Ok(a) => a, Err(e) => { eprintln!("Failed to fetch wallet aliases: {e}"); exit(2); } }; let table = prettytable_balance(&balmap, &aliases_map); if table.is_empty() { println!("No unspent balances found"); } else { println!("{table}"); } Ok(()) } DaoSubcmd::Mint { name } => { let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, Some(blockchain_config.endpoint), &ex, args.fun, ) .await; let tx = match drk.dao_mint(&name).await { Ok(tx) => tx, Err(e) => { eprintln!("Failed to mint DAO: {e}"); exit(2); } }; println!("{}", base64::encode(&serialize_async(&tx).await)); drk.stop_rpc_client().await } DaoSubcmd::ProposeTransfer { name, duration, amount, token, recipient, spend_hook, user_data, } => { let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, Some(blockchain_config.endpoint), &ex, args.fun, ) .await; if let Err(e) = f64::from_str(&amount) { eprintln!("Invalid amount: {e}"); exit(2); } let rcpt = match Address::from_str(&recipient) { Ok(r) => r, Err(e) => { eprintln!("Invalid recipient: {e}"); exit(2); } }; if rcpt.network() != drk.network { eprintln!("Recipient address prefix mismatch"); exit(2); } let token_id = match drk.get_token(token).await { Ok(t) => t, Err(e) => { eprintln!("Invalid token alias: {e}"); exit(2); } }; let spend_hook = match spend_hook { Some(s) => match FuncId::from_str(&s) { Ok(s) => Some(s), Err(e) => { eprintln!("Invalid spend hook: {e}"); exit(2); } }, None => None, }; let user_data = match user_data { Some(u) => { let bytes: [u8; 32] = match bs58::decode(&u).into_vec()?.try_into() { Ok(b) => b, Err(e) => { eprintln!("Invalid user data: {e:?}"); exit(2); } }; match pallas::Base::from_repr(bytes).into() { Some(v) => Some(v), None => { eprintln!("Invalid user data"); exit(2); } } } None => None, }; let proposal = match drk .dao_propose_transfer( &name, duration, &amount, token_id, *rcpt.public_key(), spend_hook, user_data, ) .await { Ok(p) => p, Err(e) => { eprintln!("Failed to create DAO transfer proposal: {e}"); exit(2); } }; println!("Generated proposal: {}", proposal.bulla()); drk.stop_rpc_client().await } DaoSubcmd::ProposeGeneric { name, duration, user_data } => { let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, Some(blockchain_config.endpoint), &ex, args.fun, ) .await; let user_data = match user_data { Some(u) => { let bytes: [u8; 32] = match bs58::decode(&u).into_vec()?.try_into() { Ok(b) => b, Err(e) => { eprintln!("Invalid user data: {e:?}"); exit(2); } }; match pallas::Base::from_repr(bytes).into() { Some(v) => Some(v), None => { eprintln!("Invalid user data"); exit(2); } } } None => None, }; let proposal = match drk.dao_propose_generic(&name, duration, user_data).await { Ok(p) => p, Err(e) => { eprintln!("Failed to create DAO generic proposal: {e}"); exit(2); } }; println!("Generated proposal: {}", proposal.bulla()); drk.stop_rpc_client().await } DaoSubcmd::Proposals { name } => { let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, None, &ex, args.fun, ) .await; let proposals = drk.get_dao_proposals(&name).await?; for (i, proposal) in proposals.iter().enumerate() { println!("{i}. {}", proposal.bulla()); } Ok(()) } DaoSubcmd::Proposal { bulla, export, mint_proposal } => { let bulla = match DaoProposalBulla::from_str(&bulla) { Ok(b) => b, Err(e) => { eprintln!("Invalid proposal bulla: {e}"); exit(2); } }; let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, Some(blockchain_config.endpoint), &ex, args.fun, ) .await; let proposal = drk.get_dao_proposal_by_bulla(&bulla).await?; if export { // Retrieve the DAO let dao = drk.get_dao_by_bulla(&proposal.proposal.dao_bulla).await?; // Encypt the proposal let enc_note = AeadEncryptedNote::encrypt( &proposal, &dao.params.dao.proposals_public_key, &mut OsRng, ) .unwrap(); // Export it to base64 println!("{}", base64::encode(&serialize_async(&enc_note).await)); return drk.stop_rpc_client().await } if mint_proposal { // Identify proposal type by its auth calls for call in &proposal.proposal.auth_calls { // We only support transfer right now if call.function_code == DaoFunction::AuthMoneyTransfer as u8 { let tx = match drk.dao_transfer_proposal_tx(&proposal).await { Ok(tx) => tx, Err(e) => { eprintln!("Failed to create DAO transfer proposal: {e}"); exit(2); } }; println!("{}", base64::encode(&serialize_async(&tx).await)); return drk.stop_rpc_client().await } } // If proposal has no auth calls, we consider it a generic one if proposal.proposal.auth_calls.is_empty() { let tx = match drk.dao_generic_proposal_tx(&proposal).await { Ok(tx) => tx, Err(e) => { eprintln!("Failed to create DAO generic proposal: {e}"); exit(2); } }; println!("{}", base64::encode(&serialize_async(&tx).await)); return drk.stop_rpc_client().await } eprintln!("Unsuported DAO proposal"); exit(2); } println!("{proposal}"); let mut contract_calls = "\nInvoked contracts:\n".to_string(); for call in proposal.proposal.auth_calls { contract_calls.push_str(&format!( "\tContract: {}\n\tFunction: {}\n\tData: ", call.contract_id, call.function_code )); if call.auth_data.is_empty() { contract_calls.push_str("-\n"); continue; } if call.function_code == DaoFunction::AuthMoneyTransfer as u8 { // We know that the plaintext data live in the data plaintext vec if proposal.data.is_none() { contract_calls.push_str("-\n"); continue; } let coin: CoinAttributes = deserialize_async(proposal.data.as_ref().unwrap()).await?; let recipient: Address = StandardAddress::from_public(drk.network, coin.public_key).into(); let spend_hook = if coin.spend_hook == FuncId::none() { "-".to_string() } else { format!("{}", coin.spend_hook) }; let user_data = if coin.user_data == pallas::Base::ZERO { "-".to_string() } else { format!("{:?}", coin.user_data) }; 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", "Recipient", recipient, "Amount", coin.value, encode_base10(coin.value, BALANCE_BASE10_DECIMALS), "Token", coin.token_id, "Spend hook", spend_hook, "User data", user_data, "Blind", coin.blind)); } } println!("{contract_calls}"); let votes = drk.get_dao_proposal_votes(&bulla).await?; let mut total_yes_vote_value = 0; let mut total_no_vote_value = 0; let mut total_all_vote_value = 0; let mut table = Table::new(); table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); table.set_titles(row!["Transaction", "Tokens", "Vote"]); for vote in votes { let vote_option = if vote.vote_option { total_yes_vote_value += vote.all_vote_value; "Yes" } else { total_no_vote_value += vote.all_vote_value; "No" }; total_all_vote_value += vote.all_vote_value; table.add_row(row![ vote.tx_hash, encode_base10(vote.all_vote_value, BALANCE_BASE10_DECIMALS), vote_option ]); } let outcome = if table.is_empty() { println!("Votes: No votes found"); "Unknown" } else { println!("Votes:"); println!("{table}"); println!( "Total tokens votes: {}", encode_base10(total_all_vote_value, BALANCE_BASE10_DECIMALS) ); let approval_ratio = (total_yes_vote_value as f64 * 100.0) / total_all_vote_value as f64; println!( "Total tokens Yes votes: {} ({approval_ratio:.2}%)", encode_base10(total_yes_vote_value, BALANCE_BASE10_DECIMALS) ); println!( "Total tokens No votes: {} ({:.2}%)", encode_base10(total_no_vote_value, BALANCE_BASE10_DECIMALS), (total_no_vote_value as f64 * 100.0) / total_all_vote_value as f64 ); let dao = drk.get_dao_by_bulla(&proposal.proposal.dao_bulla).await?; if total_all_vote_value >= dao.params.dao.quorum && approval_ratio >= (dao.params.dao.approval_ratio_quot / dao.params.dao.approval_ratio_base) as f64 { "Approved" } else { "Rejected" } }; if let Some(exec_tx_hash) = proposal.exec_tx_hash { println!("Proposal was executed on transaction: {exec_tx_hash}"); return drk.stop_rpc_client().await } // Retrieve next block height and current block time target, // to compute their window. let next_block_height = drk.get_next_block_height().await?; let block_target = drk.get_block_target().await?; let current_window = blockwindow(next_block_height, block_target); let end_time = proposal.proposal.creation_blockwindow + proposal.proposal.duration_blockwindows; let (voting_status, proposal_status_message) = if current_window < end_time { ("Ongoing", format!("Current proposal outcome: {outcome}")) } else { ("Concluded", format!("Proposal outcome: {outcome}")) }; println!("Voting status: {voting_status}"); println!("{proposal_status_message}"); drk.stop_rpc_client().await } DaoSubcmd::ProposalImport => { let mut buf = String::new(); stdin().read_to_string(&mut buf)?; let Some(bytes) = base64::decode(buf.trim()) else { eprintln!("Failed to decode encrypted proposal data"); exit(2); }; let encrypted_proposal: AeadEncryptedNote = deserialize_async(&bytes).await?; let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, None, &ex, args.fun, ) .await; // Retrieve all DAOs to try to decrypt the proposal let daos = drk.get_daos().await?; for dao in &daos { // Check if we have the proposals key let Some(proposals_secret_key) = dao.params.proposals_secret_key else { continue }; // Try to decrypt the proposal let Ok(proposal) = encrypted_proposal.decrypt::(&proposals_secret_key) else { continue }; let proposal = match drk.get_dao_proposal_by_bulla(&proposal.bulla()).await { Ok(p) => { let mut our_proposal = p; our_proposal.data = proposal.data; our_proposal } Err(_) => proposal, }; return drk.put_dao_proposal(&proposal).await } eprintln!("Couldn't decrypt the proposal with out DAO keys"); exit(2); } DaoSubcmd::Vote { bulla, vote, vote_weight } => { let bulla = match DaoProposalBulla::from_str(&bulla) { Ok(b) => b, Err(e) => { eprintln!("Invalid proposal bulla: {e}"); exit(2); } }; if vote > 1 { eprintln!("Vote can be either 0 (NO) or 1 (YES)"); exit(2); } let vote = vote != 0; let weight = match vote_weight { Some(w) => { if let Err(e) = f64::from_str(&w) { eprintln!("Invalid vote weight: {e}"); exit(2); } Some(decode_base10(&w, BALANCE_BASE10_DECIMALS, true)?) } None => None, }; let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, Some(blockchain_config.endpoint), &ex, args.fun, ) .await; let tx = match drk.dao_vote(&bulla, vote, weight).await { Ok(tx) => tx, Err(e) => { eprintln!("Failed to create DAO Vote transaction: {e}"); exit(2); } }; println!("{}", base64::encode(&serialize_async(&tx).await)); drk.stop_rpc_client().await } DaoSubcmd::Exec { bulla, early } => { let bulla = match DaoProposalBulla::from_str(&bulla) { Ok(b) => b, Err(e) => { eprintln!("Invalid proposal bulla: {e}"); exit(2); } }; let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, Some(blockchain_config.endpoint), &ex, args.fun, ) .await; let proposal = drk.get_dao_proposal_by_bulla(&bulla).await?; // Identify proposal type by its auth calls for call in &proposal.proposal.auth_calls { // We only support transfer right now if call.function_code == DaoFunction::AuthMoneyTransfer as u8 { let tx = match drk.dao_exec_transfer(&proposal, early).await { Ok(tx) => tx, Err(e) => { eprintln!("Failed to execute DAO transfer proposal: {e}"); exit(2); } }; println!("{}", base64::encode(&serialize_async(&tx).await)); return drk.stop_rpc_client().await } } // If proposal has no auth calls, we consider it a generic one if proposal.proposal.auth_calls.is_empty() { let tx = match drk.dao_exec_generic(&proposal, early).await { Ok(tx) => tx, Err(e) => { eprintln!("Failed to execute DAO generic proposal: {e}"); exit(2); } }; println!("{}", base64::encode(&serialize_async(&tx).await)); return drk.stop_rpc_client().await } eprintln!("Unsuported DAO proposal"); exit(2); } DaoSubcmd::SpendHook => { let spend_hook = FuncRef { contract_id: *DAO_CONTRACT_ID, func_code: DaoFunction::Exec as u8 } .to_func_id(); println!("{spend_hook}"); Ok(()) } DaoSubcmd::MiningConfig { name } => { let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, None, &ex, args.fun, ) .await; let mut output = vec![]; if let Err(e) = drk.dao_mining_config(&name, &mut output).await { print_output(&output); eprintln!("Failed to generate DAO mining configuration: {e}"); exit(2); } print_output(&output); Ok(()) } }, Subcmd::AttachFee => { let mut tx = parse_tx_from_stdin().await?; let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, Some(blockchain_config.endpoint), &ex, args.fun, ) .await; if let Err(e) = drk.attach_fee(&mut tx).await { eprintln!("Failed to attach the fee call to the transaction: {e}"); exit(2); }; println!("{}", base64::encode(&serialize_async(&tx).await)); drk.stop_rpc_client().await } Subcmd::TxFromCalls { calls_map } => { // Parse calls let calls = parse_calls_from_stdin().await?; if calls.is_empty() { eprintln!("No calls were parsed"); exit(1); } // If there is a given map, parse it, otherwise construct a // linear map. let calls_map = match calls_map { Some(cmap) => match parse_tree(&cmap) { Ok(v) => v, Err(e) => { eprintln!("Failed parsing calls map: {e}"); exit(1); } }, None => { let mut calls_map = Vec::with_capacity(calls.len()); for (i, _) in calls.iter().enumerate() { calls_map.push((i, vec![])); } calls_map } }; if calls_map.len() != calls.len() { eprintln!("Calls map size not equal to parsed calls"); exit(1); } // Create a transaction from the mapped calls. let (mut tx_builder, signature_secrets) = tx_from_calls_mapped(&calls, &calls_map)?; // Now build and sign the tx let mut tx = tx_builder.build()?; let sigs = tx.create_sigs(&signature_secrets)?; tx.signatures.push(sigs); println!("{}", base64::encode(&serialize_async(&tx).await)); Ok(()) } Subcmd::Inspect => { let tx = parse_tx_from_stdin().await?; println!("{tx:#?}"); Ok(()) } Subcmd::Broadcast => { let tx = parse_tx_from_stdin().await?; let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, Some(blockchain_config.endpoint), &ex, args.fun, ) .await; if let Err(e) = drk.simulate_tx(&tx).await { eprintln!("Failed to simulate tx: {e}"); exit(2); }; let mut output = vec![]; if let Err(e) = drk.mark_tx_spend(&tx, &mut output).await { print_output(&output); eprintln!("Failed to mark transaction coins as spent: {e}"); exit(2); }; let txid = match drk.broadcast_tx(&tx, &mut output).await { Ok(t) => t, Err(e) => { print_output(&output); eprintln!("Failed to broadcast transaction: {e}"); exit(2); } }; print_output(&output); println!("Transaction ID: {txid}"); drk.stop_rpc_client().await } Subcmd::Scan { reset } => { let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, Some(blockchain_config.endpoint), &ex, args.fun, ) .await; if let Some(height) = reset { let mut buf = vec![]; if let Err(e) = drk.reset_to_height(height, &mut buf).await { print_output(&buf); eprintln!("Failed during wallet reset: {e}"); exit(2); } print_output(&buf); } if let Err(e) = drk.scan_blocks(&mut vec![], None, &true).await { eprintln!("Failed during scanning: {e}"); exit(2); } println!("Finished scanning blockchain"); drk.stop_rpc_client().await } Subcmd::Explorer { command } => match command { ExplorerSubcmd::FetchTx { tx_hash, encode } => { let tx_hash = TransactionHash(*blake3::Hash::from_hex(&tx_hash)?.as_bytes()); let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, Some(blockchain_config.endpoint), &ex, args.fun, ) .await; let tx = match drk.get_tx(&tx_hash).await { Ok(tx) => tx, Err(e) => { eprintln!("Failed to fetch transaction: {e}"); exit(2); } }; let Some(tx) = tx else { println!("Transaction was not found"); exit(1); }; // Make sure the tx is correct assert_eq!(tx.hash(), tx_hash); if encode { println!("{}", base64::encode(&serialize_async(&tx).await)); exit(1) } println!("Transaction ID: {tx_hash}"); println!("{tx:?}"); drk.stop_rpc_client().await } ExplorerSubcmd::SimulateTx => { let tx = parse_tx_from_stdin().await?; let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, Some(blockchain_config.endpoint), &ex, args.fun, ) .await; let is_valid = match drk.simulate_tx(&tx).await { Ok(b) => b, Err(e) => { eprintln!("Failed to simulate tx: {e}"); exit(2); } }; println!("Transaction ID: {}", tx.hash()); println!("State: {}", if is_valid { "valid" } else { "invalid" }); drk.stop_rpc_client().await } ExplorerSubcmd::TxsHistory { tx_hash, encode } => { let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, None, &ex, args.fun, ) .await; if let Some(c) = tx_hash { let (tx_hash, status, block_height, tx) = drk.get_tx_history_record(&c).await?; if encode { println!("{}", base64::encode(&serialize_async(&tx).await)); exit(1) } println!("Transaction ID: {tx_hash}"); println!("Status: {status}"); match block_height { Some(block_height) => println!("Block height: {block_height}"), None => println!("Block height: -"), } println!("{tx:?}"); return Ok(()) } let map = match drk.get_txs_history() { Ok(m) => m, Err(e) => { eprintln!("Failed to retrieve transactions history records: {e}"); exit(2); } }; // Create a prettytable with the new data: let mut table = Table::new(); table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); table.set_titles(row!["Transaction Hash", "Status", "Block Height"]); for (txs_hash, status, block_height) in map.iter() { let block_height = match block_height { Some(block_height) => block_height.to_string(), None => String::from("-"), }; table.add_row(row![txs_hash, status, block_height]); } if table.is_empty() { println!("No transactions found"); } else { println!("{table}"); } Ok(()) } ExplorerSubcmd::ClearReverted => { let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, None, &ex, args.fun, ) .await; let mut output = vec![]; if let Err(e) = drk.remove_reverted_txs(&mut output) { print_output(&output); eprintln!("Failed to remove reverted transactions: {e}"); exit(2); }; print_output(&output); Ok(()) } ExplorerSubcmd::ScannedBlocks { height } => { let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, None, &ex, args.fun, ) .await; if let Some(height) = height { let (hash, signing_key) = match drk.get_scanned_block(&height) { Ok(p) => p, Err(e) => { eprintln!("Failed to retrieve scanned block record: {e}"); exit(2); } }; println!("Height: {height}"); println!("Hash: {hash}"); println!("Signing key: {signing_key}"); return Ok(()) } let map = match drk.get_scanned_block_records() { Ok(m) => m, Err(e) => { eprintln!("Failed to retrieve scanned blocks records: {e}"); exit(2); } }; let table = prettytable_scanned_blocks(&map); if table.is_empty() { println!("No scanned blocks records found"); } else { println!("{table}"); } Ok(()) } ExplorerSubcmd::MiningConfig => { let (config, recipient, spend_hook, user_data) = parse_mining_config_from_stdin().await?; let mut output = vec![]; display_mining_config(&config, &recipient, &spend_hook, &user_data, &mut output); print_output(&output); Ok(()) } }, Subcmd::Alias { command } => match command { AliasSubcmd::Add { alias, token } => { if alias.chars().count() > 5 { eprintln!("Error: Alias exceeds 5 characters"); exit(2); } let token_id = match TokenId::from_str(token.as_str()) { Ok(t) => t, Err(e) => { eprintln!("Invalid Token ID: {e}"); exit(2); } }; let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, None, &ex, args.fun, ) .await; let mut output = vec![]; if let Err(e) = drk.add_alias(alias, token_id, &mut output).await { print_output(&output); eprintln!("Failed to add alias: {e}"); exit(2); } print_output(&output); Ok(()) } AliasSubcmd::Show { alias, token } => { let token_id = match token { Some(t) => match TokenId::from_str(t.as_str()) { Ok(t) => Some(t), Err(e) => { eprintln!("Invalid Token ID: {e}"); exit(2); } }, None => None, }; let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, None, &ex, args.fun, ) .await; let map = drk.get_aliases(alias, token_id).await?; let table = prettytable_aliases(&map); if table.is_empty() { println!("No aliases found"); } else { println!("{table}"); } Ok(()) } AliasSubcmd::Remove { alias } => { let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, None, &ex, args.fun, ) .await; let mut output = vec![]; if let Err(e) = drk.remove_alias(alias, &mut output).await { print_output(&output); eprintln!("Failed to remove alias: {e}"); exit(2); } print_output(&output); Ok(()) } }, Subcmd::Token { command } => match command { TokenSubcmd::Import { secret_key, token_blind } => { let mint_authority = match SecretKey::from_str(&secret_key) { Ok(ma) => ma, Err(e) => { eprintln!("Invalid mint authority: {e}"); exit(2); } }; let token_blind = match BaseBlind::from_str(&token_blind) { Ok(tb) => tb, Err(e) => { eprintln!("Invalid token blind: {e}"); exit(2); } }; let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, None, &ex, args.fun, ) .await; let token_id = drk.import_mint_authority(mint_authority, token_blind).await?; println!("Successfully imported mint authority for token ID: {token_id}"); Ok(()) } TokenSubcmd::GenerateMint => { let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, None, &ex, args.fun, ) .await; let mint_authority = SecretKey::random(&mut OsRng); let token_blind = BaseBlind::random(&mut OsRng); let token_id = drk.import_mint_authority(mint_authority, token_blind).await?; println!("Successfully imported mint authority for token ID: {token_id}"); Ok(()) } TokenSubcmd::List => { let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, None, &ex, args.fun, ) .await; let tokens = drk.get_mint_authorities().await?; let aliases_map = match drk.get_aliases_mapped_by_token().await { Ok(map) => map, Err(e) => { eprintln!("Failed to fetch wallet aliases: {e}"); exit(2); } }; let table = prettytable_tokenlist(&tokens, &aliases_map); if table.is_empty() { println!("No tokens found"); } else { println!("{table}"); } Ok(()) } TokenSubcmd::Mint { token, amount, recipient, spend_hook, user_data } => { let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, Some(blockchain_config.endpoint), &ex, args.fun, ) .await; if let Err(e) = f64::from_str(&amount) { eprintln!("Invalid amount: {e}"); exit(2); } let rcpt = match Address::from_str(&recipient) { Ok(r) => r, Err(e) => { eprintln!("Invalid recipient: {e}"); exit(2); } }; if rcpt.network() != drk.network { eprintln!("Recipient address prefix mismatch"); exit(2); } let token_id = match drk.get_token(token).await { Ok(t) => t, Err(e) => { eprintln!("Invalid Token ID: {e}"); exit(2); } }; let spend_hook = match spend_hook { Some(s) => match FuncId::from_str(&s) { Ok(s) => Some(s), Err(e) => { eprintln!("Invalid spend hook: {e}"); exit(2); } }, None => None, }; let user_data = match user_data { Some(u) => { let bytes: [u8; 32] = match bs58::decode(&u).into_vec()?.try_into() { Ok(b) => b, Err(e) => { eprintln!("Invalid user data: {e:?}"); exit(2); } }; match pallas::Base::from_repr(bytes).into() { Some(v) => Some(v), None => { eprintln!("Invalid user data"); exit(2); } } } None => None, }; let tx = match drk .mint_token(&amount, *rcpt.public_key(), token_id, spend_hook, user_data) .await { Ok(tx) => tx, Err(e) => { eprintln!("Failed to create token mint transaction: {e}"); exit(2); } }; println!("{}", base64::encode(&serialize_async(&tx).await)); drk.stop_rpc_client().await } TokenSubcmd::Freeze { token } => { let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, Some(blockchain_config.endpoint), &ex, args.fun, ) .await; let token_id = match drk.get_token(token).await { Ok(t) => t, Err(e) => { eprintln!("Invalid Token ID: {e}"); exit(2); } }; let tx = match drk.freeze_token(token_id).await { Ok(tx) => tx, Err(e) => { eprintln!("Failed to create token freeze transaction: {e}"); exit(2); } }; println!("{}", base64::encode(&serialize_async(&tx).await)); drk.stop_rpc_client().await } }, Subcmd::Contract { command } => match command { ContractSubcmd::GenerateDeploy => { let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, None, &ex, args.fun, ) .await; let mut output = vec![]; if let Err(e) = drk.deploy_auth_keygen(&mut output).await { print_output(&output); eprintln!("Error creating deploy auth keypair: {e}"); exit(2); } print_output(&output); Ok(()) } ContractSubcmd::List { contract_id } => { let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, None, &ex, args.fun, ) .await; if let Some(contract_id) = contract_id { let contract_id = match ContractId::from_str(&contract_id) { Ok(d) => d, Err(e) => { eprintln!("Invalid contract id: {e}"); exit(2); } }; let history = drk.get_deploy_auth_history(&contract_id).await?; let table = prettytable_contract_history(&history); if table.is_empty() { println!("No history records found"); } else { println!("{table}"); } return Ok(()) } let auths = drk.list_deploy_auth().await?; let table = prettytable_contract_auth(&auths); if table.is_empty() { println!("No deploy authorities found"); } else { println!("{table}"); } Ok(()) } ContractSubcmd::ExportData { tx_hash } => { let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, None, &ex, args.fun, ) .await; let pair = drk.get_deploy_history_record_data(&tx_hash).await?; println!("{}", base64::encode(&serialize_async(&pair).await)); Ok(()) } ContractSubcmd::Deploy { deploy_auth, wasm_path, deploy_ix } => { // Parse the deployment authority contract id let deploy_auth = match ContractId::from_str(&deploy_auth) { Ok(d) => d, Err(e) => { eprintln!("Invalid deploy authority: {e}"); exit(2); } }; // Read the wasm bincode and deploy instruction let wasm_bin = smol::fs::read(expand_path(&wasm_path)?).await?; let deploy_ix = match deploy_ix { Some(p) => smol::fs::read(expand_path(&p)?).await?, None => vec![], }; let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, Some(blockchain_config.endpoint), &ex, args.fun, ) .await; let tx = match drk.deploy_contract(&deploy_auth, wasm_bin, deploy_ix).await { Ok(tx) => tx, Err(e) => { eprintln!("Error creating contract deployment tx: {e}"); exit(2); } }; println!("{}", base64::encode(&serialize_async(&tx).await)); drk.stop_rpc_client().await } ContractSubcmd::Lock { deploy_auth } => { // Parse the deployment authority contract id let deploy_auth = match ContractId::from_str(&deploy_auth) { Ok(d) => d, Err(e) => { eprintln!("Invalid deploy authority: {e}"); exit(2); } }; let drk = new_wallet( network, blockchain_config.cache_path, blockchain_config.wallet_path, blockchain_config.wallet_pass, Some(blockchain_config.endpoint), &ex, args.fun, ) .await; let tx = match drk.lock_contract(&deploy_auth).await { Ok(tx) => tx, Err(e) => { eprintln!("Error creating contract lock tx: {e}"); exit(2); } }; println!("{}", base64::encode(&serialize_async(&tx).await)); drk.stop_rpc_client().await } }, } }