/* 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::{ fs, io::{stdin, Read}, process::exit, str::FromStr, sync::Arc, time::Instant, }; use prettytable::{format, row, Table}; use smol::stream::StreamExt; use structopt_toml::{serde::Deserialize, structopt::StructOpt, StructOptToml}; use url::Url; use darkfi::{ async_daemonize, cli_desc, rpc::{client::RpcClient, jsonrpc::JsonRequest, util::JsonValue}, tx::Transaction, util::{parse::encode_base10, path::expand_path}, Result, }; use darkfi_money_contract::model::Coin; use darkfi_sdk::{ crypto::TokenId, pasta::{group::ff::PrimeField, pallas}, }; use darkfi_serial::{deserialize, serialize}; /// Error codes mod error; /// darkfid JSON-RPC related methods mod rpc; /// CLI utility functions mod cli_util; use cli_util::kaching; /// Wallet functionality related to Money mod money; use money::BALANCE_BASE10_DECIMALS; /// Wallet functionality related to Dao mod dao; /// Wallet functionality related to transactions history mod txs_history; /// Wallet database operations handler mod walletdb; use walletdb::{WalletDb, WalletPtr}; const CONFIG_FILE: &str = "drk_config.toml"; const CONFIG_FILE_CONTENTS: &str = include_str!("../drk_config.toml"); #[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(long, default_value = "~/.local/darkfi/drk/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:8340")] /// darkfid JSON-RPC endpoint endpoint: Url, #[structopt(subcommand)] /// Sub command to execute command: Subcmd, #[structopt(short, long)] /// Set log file to ouput into log: Option, #[structopt(short, parse(from_occurrences))] /// Increase verbosity (-vvv supported) verbose: u8, } #[derive(Clone, Debug, Deserialize, StructOpt)] enum Subcmd { /// Fun Kaching, /// Send a ping request to the darkfid RPC endpoint Ping, // TODO: shell completions /// 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, }, /// Unspend a coin Unspend { /// base58-encoded coin to mark as unspent coin: String, }, // TODO: Transfer // TODO: OTC /// 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, // TODO: DAO /// Scan the blockchain and parse relevant transactions Scan { #[structopt(long)] /// Reset Merkle tree and start scanning from first block reset: bool, #[structopt(long)] /// List all available checkpoints list: bool, #[structopt(long)] /// Reset Merkle tree to checkpoint index and start scanning checkpoint: 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, }, // TODO: Token } #[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, }, } #[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, }, } /// CLI-util structure pub struct Drk { /// Wallet database operations handler pub wallet: WalletPtr, /// JSON-RPC client to execute requests to darkfid daemon pub rpc_client: RpcClient, } impl Drk { async fn new( wallet_path: String, wallet_pass: String, endpoint: Url, ex: Arc>, ) -> Result { // Initialize wallet let wallet_path = expand_path(&wallet_path)?; if !wallet_path.exists() { if let Some(parent) = wallet_path.parent() { fs::create_dir_all(parent)?; } } let wallet = match WalletDb::new(Some(wallet_path), Some(&wallet_pass)) { Ok(w) => w, Err(e) => { eprintln!("Error initializing wallet: {e:?}"); exit(2); } }; // Initialize rpc client let rpc_client = RpcClient::new(endpoint, ex).await?; Ok(Self { wallet, rpc_client }) } /// Initialize wallet with tables for drk async fn initialize_wallet(&self) -> Result<()> { let wallet_schema = include_str!("../wallet.sql"); if let Err(e) = self.wallet.exec_batch_sql(wallet_schema).await { eprintln!("Error initializing wallet: {e:?}"); exit(2); } Ok(()) } /// Auxilliary function to ping configured darkfid daemon for liveness. async fn ping(&self) -> Result<()> { eprintln!("Executing ping request to darkfid..."); let latency = Instant::now(); let req = JsonRequest::new("ping", JsonValue::Array(vec![])); let rep = self.rpc_client.oneshot_request(req).await?; let latency = latency.elapsed(); eprintln!("Got reply: {rep:?}"); eprintln!("Latency: {latency:?}"); Ok(()) } } async_daemonize!(realmain); async fn realmain(args: Args, ex: Arc>) -> Result<()> { match args.command { Subcmd::Kaching => { kaching().await; Ok(()) } Subcmd::Ping => { let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; drk.ping().await } 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 = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; if initialize { drk.initialize_wallet().await?; 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); } 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() { eprintln!("No unspent balances found"); } else { eprintln!("{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); } }; eprintln!("{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() { eprintln!("No addresses found"); } else { eprintln!("{table}"); } return Ok(()) } if let Some(idx) = default_address { if let Err(e) = drk.set_default_address(idx).await { eprintln!("Failed to set default address: {e:?}"); exit(2); } return Ok(()) } if secrets { let v = drk.get_money_secrets().await?; for i in v { eprintln!("{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(&bytes) else { eprintln!("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 { eprintln!("{key}"); } return Ok(()) } if tree { let tree = drk.get_money_tree().await?; eprintln!("{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" ]); let zero = pallas::Base::zero(); 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 != zero { bs58::encode(&serialize(&coin.0.note.spend_hook)).into_string().to_string() } else { String::from("-") }; let user_data = if coin.0.note.user_data != zero { bs58::encode(&serialize(&coin.0.note.user_data)).into_string().to_string() } else { String::from("-") }; table.add_row(row![ bs58::encode(&serialize(&coin.0.coin.inner())).into_string().to_string(), coin.1, coin.0.note.token_id, aliases, format!("{} ({})", coin.0.note.value, encode_base10(coin.0.note.value, 8)), spend_hook, user_data ]); } eprintln!("{table}"); return Ok(()) } unreachable!() } 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 = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; if let Err(e) = drk.unspend_coin(&coin).await { eprintln!("Failed to mark coin as unspent: {e:?}"); exit(2); } Ok(()) } Subcmd::Inspect => { let mut buf = String::new(); stdin().read_to_string(&mut buf)?; let bytes = bs58::decode(&buf.trim()).into_vec()?; let tx: Transaction = deserialize(&bytes)?; eprintln!("{tx:#?}"); Ok(()) } Subcmd::Broadcast => { eprintln!("Reading transaction from stdin..."); let mut buf = String::new(); stdin().read_to_string(&mut buf)?; let bytes = bs58::decode(&buf.trim()).into_vec()?; let tx = deserialize(&bytes)?; let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; let txid = match drk.broadcast_tx(&tx).await { Ok(t) => t, Err(e) => { eprintln!("Failed to broadcast transaction: {e:?}"); exit(2); } }; eprintln!("Transaction ID: {txid}"); Ok(()) } Subcmd::Subscribe => { let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint.clone(), ex.clone()) .await?; if let Err(e) = drk.subscribe_blocks(args.endpoint, ex).await { eprintln!("Block subscription failed: {e:?}"); exit(2); } Ok(()) } Subcmd::Scan { reset, list, checkpoint } => { let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint.clone(), ex.clone()) .await?; if reset { eprintln!("Reset requested."); if let Err(e) = drk.scan_blocks(true).await { eprintln!("Failed during scanning: {e:?}"); exit(2); } eprintln!("Finished scanning blockchain"); return Ok(()) } if list { eprintln!("List requested."); // TODO: implement return Ok(()) } if let Some(c) = checkpoint { eprintln!("Checkpoint requested: {c}"); // TODO: implement return Ok(()) } if let Err(e) = drk.scan_blocks(false).await { eprintln!("Failed during scanning: {e:?}"); exit(2); } eprintln!("Finished scanning blockchain"); Ok(()) } Subcmd::Explorer { command } => match command { ExplorerSubcmd::FetchTx { tx_hash, full, encode } => { let tx_hash = blake3::Hash::from_hex(&tx_hash)?; let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint.clone(), ex.clone()) .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 { eprintln!("Transaction was not found"); exit(1); }; // Make sure the tx is correct assert_eq!(tx.hash()?, tx_hash); if encode { eprintln!("{}", bs58::encode(&serialize(&tx)).into_string()); exit(1) } eprintln!("Transaction ID: {tx_hash}"); if full { eprintln!("{tx:?}"); } Ok(()) } ExplorerSubcmd::SimulateTx => { eprintln!("Reading transaction from stdin..."); let mut buf = String::new(); stdin().read_to_string(&mut buf)?; let bytes = bs58::decode(&buf.trim()).into_vec()?; let tx = deserialize(&bytes)?; let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint.clone(), ex.clone()) .await?; let is_valid = match drk.simulate_tx(&tx).await { Ok(b) => b, Err(e) => { eprintln!("Failed to simulate tx: {e:?}"); exit(2); } }; eprintln!("Transaction ID: {}", tx.hash()?); eprintln!("State: {}", if is_valid { "valid" } else { "invalid" }); Ok(()) } ExplorerSubcmd::TxsHistory { tx_hash, encode } => { let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint.clone(), ex.clone()) .await?; if let Some(c) = tx_hash { let (tx_hash, status, tx) = drk.get_tx_history_record(&c).await?; if encode { println!("{}", bs58::encode(&serialize(&tx)).into_string()); exit(1) } eprintln!("Transaction ID: {tx_hash}"); eprintln!("Status: {status}"); eprintln!("{tx:?}"); return Ok(()) } let map = match drk.get_txs_history().await { 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() { eprintln!("No transactions found"); } else { eprintln!("{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 = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).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 = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).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() { eprintln!("No aliases found"); } else { eprintln!("{table}"); } Ok(()) } AliasSubcmd::Remove { alias } => { let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; if let Err(e) = drk.remove_alias(alias).await { eprintln!("Failed to remove alias: {e:?}"); exit(2); } Ok(()) } }, } }