/* This file is part of DarkFi (https://dark.fi) * * Copyright (C) 2020-2024 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, sync::Arc, }; use prettytable::{format, row, Table}; use rand::rngs::OsRng; use smol::{fs::read_to_string, stream::StreamExt}; use structopt_toml::{serde::Deserialize, structopt::StructOpt, StructOptToml}; use url::Url; use darkfi::{ async_daemonize, cli_desc, util::{ encoding::base64, 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::{ note::AeadEncryptedNote, BaseBlind, FuncId, FuncRef, Keypair, PublicKey, SecretKey, DAO_CONTRACT_ID, }, pasta::{group::ff::PrimeField, pallas}, tx::TransactionHash, }; use darkfi_serial::{deserialize_async, serialize_async}; use drk::{ cli_util::{ generate_completions, kaching, parse_token_pair, parse_tx_from_stdin, parse_value_pair, }, dao::{DaoParams, ProposalRecord}, 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() #[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 { /// 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(long)] /// Initialize wallet database initialize: bool, #[structopt(long)] /// Generate a new keypair in the wallet keygen: bool, #[structopt(long)] /// Query the wallet for known balances balance: bool, #[structopt(long)] /// Get the default address in the wallet address: bool, #[structopt(long)] /// Print all the addresses in the wallet addresses: bool, #[structopt(long)] /// Set the default address in the wallet default_address: Option, #[structopt(long)] /// Print all the secret keys from the wallet secrets: bool, #[structopt(long)] /// Import secret keys from stdin into the wallet, separated by newlines import_secrets: bool, #[structopt(long)] /// Print the Merkle tree in the wallet tree: bool, #[structopt(long)] /// Print all the coins in the wallet coins: bool, }, /// Read a transaction from stdin and mark its input coins as spent Spend, /// Unspend a coin Unspend { /// base58-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, }, /// Attach the fee call to a transaction given from stdin AttachFee, /// Inspect a transaction from stdin Inspect, /// Read a transaction from stdin and broadcast it Broadcast, /// This subscription will listen for incoming blocks from darkfid and look /// through their transactions to see if there's any that interest us. /// With `drk` we look at transactions calling the money contract so we can /// find coins sent to us and fill our wallet with the necessary metadata. Subscribe, /// DAO functionalities Dao { #[structopt(subcommand)] /// Sub command to execute command: DaoSubcmd, }, /// 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 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, }, /// Update DAO keys from stdin UpdateKeys, /// 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 base58-encoded spend hook SpendHook, } #[derive(Clone, Debug, Deserialize, StructOpt)] enum ExplorerSubcmd { /// Fetch a blockchain transaction by hash FetchTx { /// Transaction hash tx_hash: String, #[structopt(long)] /// Print the full transaction information full: bool, #[structopt(long)] /// Encode transaction to base58 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 base58 encode: bool, }, /// Remove reverted transactions from history ClearReverted, /// Fetch scanned blocks records ScannedBlocks { /// Fetch specific height record (optional) height: Option, }, } #[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 List, /// Deploy a smart contract Deploy { /// Contract ID (deploy authority) deploy_auth: u64, /// Path to contract wasm bincode wasm_path: String, /// Path to serialized deploy instruction deploy_ix: String, }, /// Lock a smart contract Lock { /// Contract ID (deploy authority) deploy_auth: u64, }, } /// 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/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:8240")] /// darkfid JSON-RPC endpoint endpoint: Url, } /// Auxiliary function to parse darkfid configuration file and extract requested /// blockchain network config. async fn parse_blockchain_config( config: Option, network: &str, ) -> Result { // 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(network_config) } /// Auxiliary function to create a `Drk` wallet for provided configuration. async fn new_wallet( wallet_path: String, wallet_pass: String, endpoint: Option, ex: Arc>, fun: bool, ) -> Drk { // Script kiddies protection if wallet_pass == "changeme" { eprintln!("Please don't use default wallet password..."); exit(2); } match Drk::new(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: Arc>) -> Result<()> { // Grab blockchain network configuration let 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::Kaching => { if !args.fun { println!("Apparently you don't like fun..."); return Ok(()) } kaching().await; Ok(()) } Subcmd::Ping => { let drk = new_wallet( blockchain_config.wallet_path, blockchain_config.wallet_pass, Some(blockchain_config.endpoint), ex, args.fun, ) .await; drk.ping().await?; drk.stop_rpc_client().await } Subcmd::Completions { shell } => generate_completions(&shell), Subcmd::Wallet { initialize, keygen, balance, address, addresses, default_address, secrets, import_secrets, tree, coins, } => { if !initialize && !keygen && !balance && !address && !addresses && default_address.is_none() && !secrets && !tree && !coins && !import_secrets { eprintln!("Error: You must use at least one flag for this subcommand"); eprintln!("Run with \"wallet -h\" to see the subcommand usage."); exit(2); } let drk = new_wallet( blockchain_config.wallet_path, blockchain_config.wallet_pass, None, ex, args.fun, ) .await; if initialize { if let Err(e) = drk.initialize_wallet().await { eprintln!("Error initializing wallet: {e:?}"); exit(2); } if let Err(e) = drk.initialize_money().await { eprintln!("Failed to initialize Money: {e:?}"); exit(2); } 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); } return Ok(()) } if keygen { if let Err(e) = drk.money_keygen().await { eprintln!("Failed to generate keypair: {e:?}"); exit(2); } return Ok(()) } if 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}"); } return Ok(()) } if address { let address = match drk.default_address().await { Ok(a) => a, Err(e) => { eprintln!("Failed to fetch default address: {e:?}"); exit(2); } }; println!("{address}"); return Ok(()) } if addresses { let addresses = drk.addresses().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!["Key ID", "Public Key", "Secret Key", "Is Default"]); for (key_id, public_key, secret_key, is_default) in addresses { let is_default = match is_default { 1 => "*", _ => "", }; table.add_row(row![key_id, public_key, secret_key, is_default]); } if table.is_empty() { println!("No addresses found"); } else { println!("{table}"); } return Ok(()) } if let Some(idx) = default_address { if let Err(e) = drk.set_default_address(idx) { eprintln!("Failed to set default address: {e:?}"); exit(2); } return Ok(()) } if secrets { let v = drk.get_money_secrets().await?; for i in v { println!("{i}"); } return Ok(()) } if import_secrets { 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 pubkeys = match drk.import_money_secrets(secrets).await { Ok(p) => p, Err(e) => { eprintln!("Failed to import secret keys into wallet: {e:?}"); exit(2); } }; for key in pubkeys { println!("{key}"); } return Ok(()) } if tree { let tree = drk.get_money_tree().await?; println!("{tree:#?}"); return Ok(()) } if coins { let coins = drk.get_coins(true).await?; let aliases_map = drk.get_aliases_mapped_by_token().await?; if coins.is_empty() { return Ok(()) } let mut table = Table::new(); table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); table.set_titles(row![ "Coin", "Spent", "Token ID", "Aliases", "Value", "Spend Hook", "User Data", "Spent TX" ]); for coin in coins { let aliases = match aliases_map.get(&coin.0.note.token_id.to_string()) { Some(a) => a, None => "-", }; let spend_hook = if coin.0.note.spend_hook != FuncId::none() { format!("{}", coin.0.note.spend_hook) } else { String::from("-") }; let user_data = if coin.0.note.user_data != pallas::Base::ZERO { bs58::encode(&serialize_async(&coin.0.note.user_data).await) .into_string() .to_string() } else { String::from("-") }; table.add_row(row![ bs58::encode(&serialize_async(&coin.0.coin.inner()).await) .into_string() .to_string(), coin.1, coin.0.note.token_id, aliases, format!( "{} ({})", coin.0.note.value, encode_base10(coin.0.note.value, BALANCE_BASE10_DECIMALS) ), spend_hook, user_data, coin.2 ]); } println!("{table}"); return Ok(()) } unreachable!() } Subcmd::Spend => { let tx = parse_tx_from_stdin().await?; let drk = new_wallet( blockchain_config.wallet_path, blockchain_config.wallet_pass, None, ex, args.fun, ) .await; if let Err(e) = drk.mark_tx_spend(&tx).await { eprintln!("Failed to mark transaction coins as spent: {e:?}"); exit(2); }; 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( 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( 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 PublicKey::from_str(&recipient) { Ok(r) => r, Err(e) => { eprintln!("Invalid recipient: {e:?}"); 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, 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( 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( 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( blockchain_config.wallet_path, blockchain_config.wallet_pass, None, ex, args.fun, ) .await; if let Err(e) = drk.inspect_swap(bytes).await { eprintln!("Failed to inspect swap: {e:?}"); exit(2); }; Ok(()) } OtcSubcmd::Sign => { let mut tx = parse_tx_from_stdin().await?; let drk = new_wallet( 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( 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( blockchain_config.wallet_path, blockchain_config.wallet_pass, None, ex, args.fun, ) .await; if let Err(e) = drk.import_dao(&name, ¶ms).await { eprintln!("Failed to import DAO: {e:?}"); exit(2); } Ok(()) } DaoSubcmd::UpdateKeys => { let mut buf = String::new(); stdin().read_to_string(&mut buf)?; let params = DaoParams::from_toml_str(&buf)?; let drk = new_wallet( blockchain_config.wallet_path, blockchain_config.wallet_pass, None, ex, args.fun, ) .await; if let Err(e) = drk.update_dao_keys(¶ms).await { eprintln!("Failed to update DAO keys: {e:?}"); exit(2); } Ok(()) } DaoSubcmd::List { name } => { let drk = new_wallet( blockchain_config.wallet_path, blockchain_config.wallet_pass, None, ex, args.fun, ) .await; if let Err(e) = drk.dao_list(&name).await { eprintln!("Failed to list DAO: {e:?}"); exit(2); } Ok(()) } DaoSubcmd::Balance { name } => { let drk = new_wallet( 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); } }; // 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}"); } Ok(()) } DaoSubcmd::Mint { name } => { let drk = new_wallet( 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( 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 PublicKey::from_str(&recipient) { Ok(r) => r, Err(e) => { eprintln!("Invalid recipient: {e:?}"); 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, 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( 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 transfer proposal: {e:?}"); exit(2); } }; println!("Generated proposal: {}", proposal.bulla()); drk.stop_rpc_client().await } DaoSubcmd::Proposals { name } => { let drk = new_wallet( 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( 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 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", coin.public_key, "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( 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( 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( 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(()) } }, Subcmd::AttachFee => { let mut tx = parse_tx_from_stdin().await?; let drk = new_wallet( 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::Inspect => { let tx = parse_tx_from_stdin().await?; println!("{tx:#?}"); Ok(()) } Subcmd::Broadcast => { let tx = parse_tx_from_stdin().await?; let drk = new_wallet( 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); }; if let Err(e) = drk.mark_tx_spend(&tx).await { eprintln!("Failed to mark transaction coins as spent: {e:?}"); exit(2); }; let txid = match drk.broadcast_tx(&tx).await { Ok(t) => t, Err(e) => { eprintln!("Failed to broadcast transaction: {e:?}"); exit(2); } }; println!("Transaction ID: {txid}"); drk.stop_rpc_client().await } Subcmd::Subscribe => { let drk = new_wallet( blockchain_config.wallet_path, blockchain_config.wallet_pass, Some(blockchain_config.endpoint.clone()), ex.clone(), args.fun, ) .await; if let Err(e) = drk.subscribe_blocks(blockchain_config.endpoint, ex).await { eprintln!("Block subscription failed: {e:?}"); exit(2); } drk.stop_rpc_client().await } Subcmd::Scan { reset } => { let drk = new_wallet( blockchain_config.wallet_path, blockchain_config.wallet_pass, Some(blockchain_config.endpoint), ex, args.fun, ) .await; if let Some(height) = reset { if let Err(e) = drk.reset_to_height(height).await { eprintln!("Failed during wallet reset: {e:?}"); exit(2); } } if let Err(e) = drk.scan_blocks().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, full, encode } => { let tx_hash = TransactionHash(*blake3::Hash::from_hex(&tx_hash)?.as_bytes()); let drk = new_wallet( 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}"); if full { println!("{tx:?}"); } drk.stop_rpc_client().await } ExplorerSubcmd::SimulateTx => { let tx = parse_tx_from_stdin().await?; let drk = new_wallet( 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( blockchain_config.wallet_path, blockchain_config.wallet_pass, None, ex, args.fun, ) .await; if let Some(c) = tx_hash { let (tx_hash, status, 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}"); 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"]); for (txs_hash, status) in map.iter() { table.add_row(row![txs_hash, status]); } if table.is_empty() { println!("No transactions found"); } else { println!("{table}"); } Ok(()) } ExplorerSubcmd::ClearReverted => { let drk = new_wallet( blockchain_config.wallet_path, blockchain_config.wallet_pass, None, ex, args.fun, ) .await; if let Err(e) = drk.remove_reverted_txs() { eprintln!("Failed to remove reverted transactions: {e:?}"); exit(2); }; Ok(()) } ExplorerSubcmd::ScannedBlocks { height } => { let drk = new_wallet( blockchain_config.wallet_path, blockchain_config.wallet_pass, None, ex, args.fun, ) .await; if let Some(h) = height { let (height, hash, _) = match drk.get_scanned_block_record(h) { Ok(ret) => ret, Err(e) => { eprintln!("Failed to retrieve scanned block record: {e:?}"); exit(2); } }; println!("Height: {height}"); println!("Hash: {hash}"); 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); } }; // 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!["Height", "Hash"]); for (height, hash, _) in map.iter() { table.add_row(row![height, hash]); } if table.is_empty() { println!("No scanned blocks records found"); } else { println!("{table}"); } 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( blockchain_config.wallet_path, blockchain_config.wallet_pass, None, ex, args.fun, ) .await; if let Err(e) = drk.add_alias(alias, token_id).await { eprintln!("Failed to add alias: {e:?}"); exit(2); } 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( blockchain_config.wallet_path, blockchain_config.wallet_pass, None, ex, args.fun, ) .await; let map = drk.get_aliases(alias, token_id).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!["Alias", "Token ID"]); for (alias, token_id) in map.iter() { table.add_row(row![alias, token_id]); } if table.is_empty() { println!("No aliases found"); } else { println!("{table}"); } Ok(()) } AliasSubcmd::Remove { alias } => { let drk = new_wallet( blockchain_config.wallet_path, blockchain_config.wallet_pass, None, ex, args.fun, ) .await; if let Err(e) = drk.remove_alias(alias).await { eprintln!("Failed to remove alias: {e:?}"); exit(2); } 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( 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( 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( 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 mut table = Table::new(); table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); table.set_titles(row![ "Token ID", "Aliases", "Mint Authority", "Token Blind", "Frozen" ]); for (token_id, authority, blind, frozen) in tokens { let aliases = match aliases_map.get(&token_id.to_string()) { Some(a) => a, None => "-", }; table.add_row(row![token_id, aliases, authority, blind, frozen]); } 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( 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 PublicKey::from_str(&recipient) { Ok(r) => r, Err(e) => { eprintln!("Invalid recipient: {e:?}"); 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, 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( 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( blockchain_config.wallet_path, blockchain_config.wallet_pass, None, ex, args.fun, ) .await; if let Err(e) = drk.deploy_auth_keygen().await { eprintln!("Error creating deploy auth keypair: {:?}", e); exit(2); } Ok(()) } ContractSubcmd::List => { let drk = new_wallet( blockchain_config.wallet_path, blockchain_config.wallet_pass, None, ex, args.fun, ) .await; let auths = drk.list_deploy_auth().await?; let mut table = Table::new(); table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); table.set_titles(row!["Index", "Contract ID", "Frozen"]); for (idx, contract_id, frozen) in auths { table.add_row(row![idx, contract_id, frozen]); } if table.is_empty() { println!("No deploy authorities found"); } else { println!("{table}"); } Ok(()) } ContractSubcmd::Deploy { deploy_auth, wasm_path, deploy_ix } => { // Read the wasm bincode and deploy instruction let wasm_bin = smol::fs::read(expand_path(&wasm_path)?).await?; let deploy_ix = smol::fs::read(expand_path(&deploy_ix)?).await?; let drk = new_wallet( blockchain_config.wallet_path, blockchain_config.wallet_pass, Some(blockchain_config.endpoint), ex, args.fun, ) .await; let mut tx = match drk.deploy_contract(deploy_auth, wasm_bin, deploy_ix).await { Ok(v) => v, Err(e) => { eprintln!("Error creating contract deployment tx: {}", e); exit(2); } }; 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 } ContractSubcmd::Lock { deploy_auth } => { let drk = new_wallet( blockchain_config.wallet_path, blockchain_config.wallet_pass, Some(blockchain_config.endpoint), ex, args.fun, ) .await; let mut tx = match drk.lock_contract(deploy_auth).await { Ok(v) => v, Err(e) => { eprintln!("Error creating contract lock tx: {}", e); exit(2); } }; 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 } }, } }