/* This file is part of DarkFi (https://dark.fi) * * Copyright (C) 2020-2025 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, Cursor, Read}, slice, str::FromStr, }; use rodio::{Decoder, OutputStreamBuilder, Sink}; use smol::channel::Sender; use structopt_toml::clap::{App, Arg, Shell, SubCommand}; use darkfi::{ cli_desc, tx::Transaction, util::{encoding::base64, parse::decode_base10}, Error, Result, }; use darkfi_money_contract::model::TokenId; use darkfi_sdk::{ crypto::{keypair::Address, pasta_prelude::PrimeField, FuncId}, pasta::pallas, }; use darkfi_serial::deserialize_async; use crate::{money::BALANCE_BASE10_DECIMALS, Drk}; /// Auxiliary function to parse a base64 encoded transaction from stdin. pub async fn parse_tx_from_stdin() -> Result { let mut buf = String::new(); stdin().read_to_string(&mut buf)?; match base64::decode(buf.trim()) { Some(bytes) => Ok(deserialize_async(&bytes).await?), None => Err(Error::ParseFailed("Failed to decode transaction")), } } /// Auxiliary function to parse a base64 encoded transaction from /// provided input or fallback to stdin if its empty. pub async fn parse_tx_from_input(input: &[String]) -> Result { match input.len() { 0 => parse_tx_from_stdin().await, 1 => match base64::decode(input[0].trim()) { Some(bytes) => Ok(deserialize_async(&bytes).await?), None => Err(Error::ParseFailed("Failed to decode transaction")), }, _ => Err(Error::ParseFailed("Multiline input provided")), } } /// Auxiliary function to parse provided string into a values pair. pub fn parse_value_pair(s: &str) -> Result<(u64, u64)> { let v: Vec<&str> = s.split(':').collect(); if v.len() != 2 { return Err(Error::ParseFailed("Invalid value pair. Use a pair such as 13.37:11.0")) } let val0 = decode_base10(v[0], BALANCE_BASE10_DECIMALS, true); let val1 = decode_base10(v[1], BALANCE_BASE10_DECIMALS, true); if val0.is_err() || val1.is_err() { return Err(Error::ParseFailed("Invalid value pair. Use a pair such as 13.37:11.0")) } Ok((val0.unwrap(), val1.unwrap())) } /// Auxiliary function to parse provided string into a tokens pair. pub async fn parse_token_pair(drk: &Drk, s: &str) -> Result<(TokenId, TokenId)> { let v: Vec<&str> = s.split(':').collect(); if v.len() != 2 { return Err(Error::ParseFailed( "Invalid token pair. Use a pair such as:\nWCKD:MLDY\nor\n\ A7f1RKsCUUHrSXA7a9ogmwg8p3bs6F47ggsW826HD4yd:FCuoMii64H5Ee4eVWBjP18WTFS8iLUJmGi16Qti1xFQ2" )) } let tok0 = drk.get_token(v[0].to_string()).await; let tok1 = drk.get_token(v[1].to_string()).await; if tok0.is_err() || tok1.is_err() { return Err(Error::ParseFailed( "Invalid token pair. Use a pair such as:\nWCKD:MLDY\nor\n\ A7f1RKsCUUHrSXA7a9ogmwg8p3bs6F47ggsW826HD4yd:FCuoMii64H5Ee4eVWBjP18WTFS8iLUJmGi16Qti1xFQ2" )) } Ok((tok0.unwrap(), tok1.unwrap())) } /// Fun police go away pub async fn kaching() { const WALLET_MP3: &[u8] = include_bytes!("../wallet.mp3"); let cursor = Cursor::new(WALLET_MP3); let Ok(stream_handle) = OutputStreamBuilder::open_default_stream() else { return }; let sink = Sink::connect_new(stream_handle.mixer()); let Ok(source) = Decoder::new(cursor) else { return }; sink.append(source); sink.sleep_until_end(); } /// Auxiliary function to generate provided shell completions. pub fn generate_completions(shell: &str) -> Result { // Sub-commands // Interactive let interactive = SubCommand::with_name("interactive").about("Enter Drk interactive shell"); // Kaching let kaching = SubCommand::with_name("kaching").about("Fun"); // Ping let ping = SubCommand::with_name("ping").about("Send a ping request to the darkfid RPC endpoint"); // Completions let shell_arg = Arg::with_name("shell").help("The Shell you want to generate script for"); let completions = SubCommand::with_name("completions") .about("Generate a SHELL completion script and print to stdout") .arg(shell_arg); // Wallet let initialize = SubCommand::with_name("initialize").about("Initialize wallet database"); let keygen = SubCommand::with_name("keygen").about("Generate a new keypair in the wallet"); let balance = SubCommand::with_name("balance").about("Query the wallet for known balances"); let address = SubCommand::with_name("address").about("Get the default address in the wallet"); let addresses = SubCommand::with_name("addresses").about("Print all the addresses in the wallet"); let index = Arg::with_name("index").help("Identifier of the address"); let default_address = SubCommand::with_name("default-address") .about("Set the default address in the wallet") .arg(index.clone()); let secrets = SubCommand::with_name("secrets").about("Print all the secret keys from the wallet"); let import_secrets = SubCommand::with_name("import-secrets") .about("Import secret keys from stdin into the wallet, separated by newlines"); let tree = SubCommand::with_name("tree").about("Print the Merkle tree in the wallet"); let coins = SubCommand::with_name("coins").about("Print all the coins in the wallet"); let spend_hook = Arg::with_name("spend-hook").help("Optional contract spend hook to use"); let user_data = Arg::with_name("user-data").help("Optional user data to use"); let mining_config = SubCommand::with_name("mining-config") .about("Print a wallet address mining configuration") .args(&[index, spend_hook.clone(), user_data.clone()]); let wallet = SubCommand::with_name("wallet").about("Wallet operations").subcommands(vec![ initialize, keygen, balance, address, addresses, default_address, secrets, import_secrets, tree, coins, mining_config, ]); // Spend let spend = SubCommand::with_name("spend") .about("Read a transaction from stdin and mark its input coins as spent"); // Unspend let coin = Arg::with_name("coin").help("base64-encoded coin to mark as unspent"); let unspend = SubCommand::with_name("unspend").about("Unspend a coin").arg(coin); // Transfer let amount = Arg::with_name("amount").help("Amount to send"); let token = Arg::with_name("token").help("Token ID to send"); let recipient = Arg::with_name("recipient").help("Recipient address"); let half_split = Arg::with_name("half-split") .long("half-split") .help("Split the output coin into two equal halves"); let transfer = SubCommand::with_name("transfer").about("Create a payment transaction").args(&[ amount.clone(), token.clone(), recipient.clone(), spend_hook.clone(), user_data.clone(), half_split, ]); // Otc let value_pair = Arg::with_name("value-pair") .short("v") .long("value-pair") .takes_value(true) .help("Value pair to send:recv (11.55:99.42)"); let token_pair = Arg::with_name("token-pair") .short("t") .long("token-pair") .takes_value(true) .help("Token pair to send:recv (f00:b4r)"); let init = SubCommand::with_name("init") .about("Initialize the first half of the atomic swap") .args(&[value_pair, token_pair]); let join = SubCommand::with_name("join").about("Build entire swap tx given the first half from stdin"); let inspect = SubCommand::with_name("inspect") .about("Inspect a swap half or the full swap tx from stdin"); let sign = SubCommand::with_name("sign").about("Sign a swap transaction given from stdin"); let otc = SubCommand::with_name("otc") .about("OTC atomic swap") .subcommands(vec![init, join, inspect, sign]); // DAO let proposer_limit = Arg::with_name("proposer-limit") .help("The minimum amount of governance tokens needed to open a proposal for this DAO"); let quorum = Arg::with_name("quorum") .help("Minimal threshold of participating total tokens needed for a proposal to pass"); let early_exec_quorum = Arg::with_name("early-exec-quorum") .help("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."); let approval_ratio = Arg::with_name("approval-ratio") .help("The ratio of winning votes/total votes needed for a proposal to pass (2 decimals)"); let gov_token_id = Arg::with_name("gov-token-id").help("DAO's governance token ID"); let create = SubCommand::with_name("create").about("Create DAO parameters").args(&[ proposer_limit, quorum, early_exec_quorum, approval_ratio, gov_token_id, ]); let view = SubCommand::with_name("view").about("View DAO data from stdin"); let name = Arg::with_name("name").help("Name identifier for the DAO"); let import = SubCommand::with_name("import") .about("Import DAO data from stdin") .args(slice::from_ref(&name)); let opt_name = Arg::with_name("dao-alias").help("Name identifier for the DAO (optional)"); let list = SubCommand::with_name("list") .about("List imported DAOs (or info about a specific one)") .args(&[opt_name]); let balance = SubCommand::with_name("balance") .about("Show the balance of a DAO") .args(slice::from_ref(&name)); let mint = SubCommand::with_name("mint") .about("Mint an imported DAO on-chain") .args(slice::from_ref(&name)); let duration = Arg::with_name("duration").help("Duration of the proposal, in block windows"); let propose_transfer = SubCommand::with_name("propose-transfer") .about("Create a transfer proposal for a DAO") .args(&[ name.clone(), duration.clone(), amount, token, recipient, spend_hook.clone(), user_data.clone(), ]); let propose_generic = SubCommand::with_name("propose-generic") .about("Create a generic proposal for a DAO") .args(&[name.clone(), duration, user_data.clone()]); let proposals = SubCommand::with_name("proposals").about("List DAO proposals").arg(&name); let bulla = Arg::with_name("bulla").help("Bulla identifier for the proposal"); let export = Arg::with_name("export").help("Encrypt the proposal and encode it to base64"); let mint_proposal = Arg::with_name("mint-proposal").help("Create the proposal transaction"); let proposal = SubCommand::with_name("proposal").about("View a DAO proposal data").args(&[ bulla.clone(), export, mint_proposal, ]); let proposal_import = SubCommand::with_name("proposal-import") .about("Import a base64 encoded and encrypted proposal from stdin"); let vote = Arg::with_name("vote").help("Vote (0 for NO, 1 for YES)"); let vote_weight = Arg::with_name("vote-weight").help("Optional vote weight (amount of governance tokens)"); let vote = SubCommand::with_name("vote").about("Vote on a given proposal").args(&[ bulla.clone(), vote, vote_weight, ]); let early = Arg::with_name("early").long("early").help("Execute the proposal early"); let exec = SubCommand::with_name("exec").about("Execute a DAO proposal").args(&[bulla, early]); let spend_hook_cmd = SubCommand::with_name("spend-hook") .about("Print the DAO contract base64-encoded spend hook"); let mining_config = SubCommand::with_name("mining-config").about("Print a DAO mining configuration").arg(name); let dao = SubCommand::with_name("dao").about("DAO functionalities").subcommands(vec![ create, view, import, list, balance, mint, propose_transfer, propose_generic, proposals, proposal, proposal_import, vote, exec, spend_hook_cmd, mining_config, ]); // AttachFee let attach_fee = SubCommand::with_name("attach-fee") .about("Attach the fee call to a transaction given from stdin"); // Inspect let inspect = SubCommand::with_name("inspect").about("Inspect a transaction from stdin"); // Broadcast let broadcast = SubCommand::with_name("broadcast").about("Read a transaction from stdin and broadcast it"); // Scan let reset = Arg::with_name("reset") .long("reset") .help("Reset wallet state to provided block height and start scanning"); let scan = SubCommand::with_name("scan") .about("Scan the blockchain and parse relevant transactions") .args(&[reset]); // Explorer let tx_hash = Arg::with_name("tx-hash").help("Transaction hash"); let encode = Arg::with_name("encode").long("encode").help("Encode transaction to base64"); let fetch_tx = SubCommand::with_name("fetch-tx") .about("Fetch a blockchain transaction by hash") .args(&[tx_hash, encode]); let simulate_tx = SubCommand::with_name("simulate-tx").about("Read a transaction from stdin and simulate it"); let tx_hash = Arg::with_name("tx-hash").help("Fetch specific history record (optional)"); let encode = Arg::with_name("encode") .long("encode") .help("Encode specific history record transaction to base64"); let txs_history = SubCommand::with_name("txs-history") .about("Fetch broadcasted transactions history") .args(&[tx_hash, encode]); let clear_reverted = SubCommand::with_name("clear-reverted").about("Remove reverted transactions from history"); let height = Arg::with_name("height").help("Fetch specific height record (optional)"); let scanned_blocks = SubCommand::with_name("scanned-blocks") .about("Fetch scanned blocks records") .args(&[height]); let mining_config = SubCommand::with_name("mining-config") .about("Read a mining configuration from stdin and display its parts"); let explorer = SubCommand::with_name("explorer").about("Explorer related subcommands").subcommands(vec![ fetch_tx, simulate_tx, txs_history, clear_reverted, scanned_blocks, mining_config, ]); // Alias let alias = Arg::with_name("alias").help("Token alias"); let token = Arg::with_name("token").help("Token to create alias for"); let add = SubCommand::with_name("add").about("Create a Token alias").args(&[alias, token]); let alias = Arg::with_name("alias") .short("a") .long("alias") .takes_value(true) .help("Token alias to search for"); let token = Arg::with_name("token") .short("t") .long("token") .takes_value(true) .help("Token to search alias for"); let show = SubCommand::with_name("show") .about( "Print alias info of optional arguments. \ If no argument is provided, list all the aliases in the wallet.", ) .args(&[alias, token]); let alias = Arg::with_name("alias").help("Token alias to remove"); let remove = SubCommand::with_name("remove").about("Remove a Token alias").arg(alias); let alias = SubCommand::with_name("alias") .about("Manage Token aliases") .subcommands(vec![add, show, remove]); // Token let secret_key = Arg::with_name("secret-key").help("Mint authority secret key"); let token_blind = Arg::with_name("token-blind").help("Mint authority token blind"); let import = SubCommand::with_name("import") .about("Import a mint authority") .args(&[secret_key, token_blind]); let generate_mint = SubCommand::with_name("generate-mint").about("Generate a new mint authority"); let list = SubCommand::with_name("list").about("List token IDs with available mint authorities"); let token = Arg::with_name("token").help("Token ID to mint"); let amount = Arg::with_name("amount").help("Amount to mint"); let recipient = Arg::with_name("recipient").help("Recipient of the minted tokens"); let mint = SubCommand::with_name("mint") .about("Mint tokens") .args(&[token, amount, recipient, spend_hook, user_data]); let token = Arg::with_name("token").help("Token ID to freeze"); let freeze = SubCommand::with_name("freeze").about("Freeze a token mint").arg(token); let token = SubCommand::with_name("token").about("Token functionalities").subcommands(vec![ import, generate_mint, list, mint, freeze, ]); // Contract let generate_deploy = SubCommand::with_name("generate-deploy").about("Generate a new deploy authority"); let contract_id = Arg::with_name("contract-id").help("Contract ID (optional)"); let list = SubCommand::with_name("list") .about("List deploy authorities in the wallet (or a specific one)") .args(&[contract_id]); let tx_hash = Arg::with_name("tx-hash").help("Record transaction hash"); let export_data = SubCommand::with_name("export-data") .about("Export a contract history record wasm bincode and deployment instruction, encoded to base64") .args(&[tx_hash]); let deploy_auth = Arg::with_name("deploy-auth").help("Contract ID (deploy authority)"); let wasm_path = Arg::with_name("wasm-path").help("Path to contract wasm bincode"); let deploy_ix = Arg::with_name("deploy-ix").help("Optional path to serialized deploy instruction"); let deploy = SubCommand::with_name("deploy").about("Deploy a smart contract").args(&[ deploy_auth.clone(), wasm_path, deploy_ix, ]); let lock = SubCommand::with_name("lock").about("Lock a smart contract").args(&[deploy_auth]); let contract = SubCommand::with_name("contract") .about("Contract functionalities") .subcommands(vec![generate_deploy, list, export_data, deploy, lock]); // Main arguments let config = Arg::with_name("config") .short("c") .long("config") .takes_value(true) .help("Configuration file to use"); let network = Arg::with_name("network") .long("network") .takes_value(true) .help("Blockchain network to use"); let command = vec![ interactive, kaching, ping, completions, wallet, spend, unspend, transfer, otc, attach_fee, inspect, broadcast, dao, scan, explorer, alias, token, contract, ]; let fun = Arg::with_name("fun") .short("f") .long("fun") .help("Flag indicating whether you want some fun in your life"); let log = Arg::with_name("log") .short("l") .long("log") .takes_value(true) .help("Set log file to ouput into"); let verbose = Arg::with_name("verbose") .short("v") .multiple(true) .help("Increase verbosity (-vvv supported)"); let mut app = App::new("drk") .about(cli_desc!()) .args(&[config, network, fun, log, verbose]) .subcommands(command); let shell = match Shell::from_str(shell) { Ok(s) => s, Err(e) => return Err(Error::Custom(e)), }; let mut buf = vec![]; app.gen_completions_to("./drk", shell, &mut buf); Ok(String::from_utf8(buf)?) } /// Auxiliary function to print provided string buffer. pub fn print_output(buf: &[String]) { for line in buf { println!("{line}"); } } /// Auxiliary function to print or insert provided messages to given /// buffer reference. If a channel sender is provided, the messages /// are send to that instead. pub async fn append_or_print( buf: &mut Vec, sender: Option<&Sender>>, print: &bool, messages: Vec, ) { // Send the messages to the channel, if provided if let Some(sender) = sender { if let Err(e) = sender.send(messages).await { let err_msg = format!("[append_or_print] Sending messages to channel failed: {e}"); if *print { println!("{err_msg}"); } else { buf.push(err_msg); } } return } // Print the messages if *print { for msg in messages { println!("{msg}"); } return } // Insert the messages in the buffer for msg in messages { buf.push(msg); } } /// Auxiliary function to parse a base64 encoded mining configuration /// from stdin. pub async fn parse_mining_config_from_stdin( ) -> Result<(String, String, Option, Option)> { let mut buf = String::new(); stdin().read_to_string(&mut buf)?; let config = buf.trim(); let (recipient, spend_hook, user_data) = match base64::decode(config) { Some(bytes) => deserialize_async(&bytes).await?, None => return Err(Error::ParseFailed("Failed to decode mining configuration")), }; Ok((config.to_string(), recipient, spend_hook, user_data)) } /// Auxiliary function to parse a base64 encoded mining configuration /// from provided input or fallback to stdin if its empty. pub async fn parse_mining_config_from_input( input: &[String], ) -> Result<(String, String, Option, Option)> { match input.len() { 0 => parse_mining_config_from_stdin().await, 1 => { let config = input[0].trim(); let (recipient, spend_hook, user_data) = match base64::decode(config) { Some(bytes) => deserialize_async(&bytes).await?, None => return Err(Error::ParseFailed("Failed to decode mining configuration")), }; Ok((config.to_string(), recipient, spend_hook, user_data)) } _ => Err(Error::ParseFailed("Multiline input provided")), } } /// Auxiliary function to display the parts of a mining configuration. pub fn display_mining_config( config: &str, recipient_str: &str, spend_hook: &Option, user_data: &Option, output: &mut Vec, ) { output.push(format!("DarkFi mining configuration address: {config}")); match Address::from_str(recipient_str) { Ok(recipient) => { output.push(format!("Recipient: {recipient_str}")); output.push(format!("Public key: {}", recipient.public_key())); output.push(format!("Network: {:?}", recipient.network())); } Err(e) => output.push(format!("Recipient: Invalid ({e})")), } let spend_hook = match spend_hook { Some(spend_hook_str) => match FuncId::from_str(spend_hook_str) { Ok(_) => String::from(spend_hook_str), Err(e) => format!("Invalid ({e})"), }, None => String::from("-"), }; output.push(format!("Spend hook: {spend_hook}")); let user_data = match user_data { Some(user_data_str) => match bs58::decode(&user_data_str).into_vec() { Ok(bytes) => match bytes.try_into() { Ok(bytes) => { if pallas::Base::from_repr(bytes).is_some().into() { String::from(user_data_str) } else { String::from("Invalid") } } Err(e) => format!("Invalid ({e:?})"), }, Err(e) => format!("Invalid ({e})"), }, None => String::from("-"), }; output.push(format!("User data: {user_data}")); }