/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2023 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,
time::Instant,
};
use anyhow::{anyhow, Context, Result};
use clap::{CommandFactory, Parser, Subcommand};
use clap_complete::{generate, Shell};
use darkfi::{tx::Transaction, util::parse::decode_base10, zk::halo2::Field};
use darkfi_sdk::{
crypto::{Coin, PublicKey, SecretKey, TokenId},
pasta::{group::ff::PrimeField, pallas},
};
use darkfi_serial::{deserialize, serialize};
use prettytable::{format, row, Table};
use rand::rngs::OsRng;
use serde_json::json;
use simplelog::{ColorChoice, TermLogger, TerminalMode};
use url::Url;
use darkfi::{
cli_desc,
rpc::{client::RpcClient, jsonrpc::JsonRequest},
util::{
cli::{get_log_config, get_log_level},
parse::encode_base10,
},
};
/// Airdrop methods
mod rpc_airdrop;
/// Payment methods
mod rpc_transfer;
/// Swap methods
mod rpc_swap;
use rpc_swap::PartialSwapData;
/// DAO methods
mod rpc_dao;
/// Token methods
mod rpc_token;
/// Blockchain methods
mod rpc_blockchain;
/// CLI utility functions
mod cli_util;
use cli_util::{parse_token_pair, parse_value_pair};
/// Wallet functionality related to drk operations
mod wallet;
/// Wallet functionality related to DAO
mod wallet_dao;
use wallet_dao::DaoParams;
/// Wallet functionality related to Money
mod wallet_money;
/// Wallet functionality related to arbitrary tokens
mod wallet_token;
/// Wallet functionality related to transactions history
mod wallet_txs_history;
#[derive(Parser)]
#[command(about = cli_desc!())]
struct Args {
#[arg(short, action = clap::ArgAction::Count)]
/// Increase verbosity (-vvv supported)
verbose: u8,
#[arg(short, long, default_value = "tcp://127.0.0.1:8340")]
/// darkfid JSON-RPC endpoint
endpoint: Url,
#[command(subcommand)]
command: Subcmd,
}
#[derive(Subcommand)]
enum Subcmd {
/// 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: Shell,
},
/// Wallet operations
Wallet {
#[arg(long)]
/// Initialize wallet with data for Money Contract (run this first)
initialize: bool,
#[arg(long)]
/// Generate a new keypair in the wallet
keygen: bool,
#[arg(long)]
/// Query the wallet for known balances
balance: bool,
#[arg(long)]
/// Get the default address in the wallet
address: bool,
#[arg(long)]
/// Print all the secret keys from the wallet
secrets: bool,
#[arg(long)]
/// Import secret keys from stdin into the wallet, separated by newlines
import_secrets: bool,
#[arg(long)]
/// Print the Merkle tree in the wallet
tree: bool,
#[arg(long)]
/// Print all the coins in the wallet
coins: bool,
},
/// Unspend a coin
Unspend {
/// base58-encoded coin to mark as unspent
coin: String,
},
/// Airdrop some tokens
Airdrop {
/// Faucet JSON-RPC endpoint
#[arg(short, long, default_value = "tls://faucetd.testnet.dark.fi:18340")]
faucet_endpoint: Url,
/// Amount to request from the faucet
amount: String,
/// Optional address to send tokens to (defaults to main address in wallet)
address: Option,
},
/// Create a payment transaction
Transfer {
/// Amount to send
amount: String,
/// Token ID to send
token: String,
/// Recipient address
recipient: String,
/// Mark if this is being sent to a DAO
#[clap(long)]
dao: bool,
/// DAO bulla, if the tokens are being sent to a DAO
dao_bulla: Option,
},
/// OTC atomic swap
#[command(subcommand)]
Otc(OtcSubcmd),
/// Inspect a transaction from stdin
Inspect,
/// Read a transaction from stdin and broadcast it
Broadcast,
/// Subscribe to incoming notifications from darkfid
#[command(subcommand)]
Subscribe(SubscribeSubcmd),
/// DAO functionalities
#[command(subcommand)]
Dao(DaoSubcmd),
/// Scan the blockchain and parse relevant transactions
Scan {
#[arg(long)]
/// Reset Merkle tree and start scanning from first slot
reset: bool,
#[arg(long)]
/// List all available checkpoints
list: bool,
#[arg(short, long)]
/// Reset Merkle tree to checkpoint index and start scanning
checkpoint: Option,
},
/// Explorer related subcommands
#[command(subcommand)]
Explorer(ExplorerSubcmd),
/// Manage Token aliases
#[command(subcommand)]
Alias(AliasSubcmd),
/// Token functionalities
#[command(subcommand)]
Token(TokenSubcmd),
}
#[derive(Subcommand)]
enum OtcSubcmd {
/// Initialize the first half of the atomic swap
Init {
/// Value pair to send:recv (11.55:99.42)
#[clap(short, long)]
value_pair: String,
/// Token pair to send:recv (f00:b4r)
#[clap(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 transaction given from stdin as the first-half
Sign,
}
#[derive(Subcommand)]
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,
/// 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 {
/// Named identifier for the DAO
dao_name: String,
},
/// List imported DAOs (or info about a specific one)
List {
/// Numeric identifier for the DAO (optional)
dao_id: Option,
},
/// Show the balance of a DAO
Balance {
/// Name or numeric identifier for the DAO
dao_alias: String,
},
/// Mint an imported DAO on-chain
Mint {
/// Name or numeric identifier for the DAO
dao_alias: String,
},
/// Create a proposal for a DAO
Propose {
/// Name or numeric identifier for the DAO
dao_alias: String,
/// Pubkey to send tokens to with proposal success
recipient: String,
/// Amount to send from DAO with proposal success
amount: String,
/// Token ID to send from DAO with proposal success
token: String,
},
/// List DAO proposals
Proposals {
/// Name or numeric identifier for the DAO
dao_alias: String,
},
/// View a DAO proposal data
Proposal {
/// Name or numeric identifier for the DAO
dao_alias: String,
/// Numeric identifier for the proposal
proposal_id: u64,
},
/// Vote on a given proposal
Vote {
/// Name or numeric identifier for the DAO
dao_alias: String,
/// Numeric identifier for the proposal
proposal_id: u64,
/// Vote (0 for NO, 1 for YES)
vote: u8,
/// Vote weight (amount of governance tokens)
vote_weight: String,
},
/// Execute a DAO proposal
Exec {
/// Name or numeric identifier for the DAO
dao_alias: String,
/// Numeric identifier for the proposal
proposal_id: u64,
},
}
#[derive(Subcommand)]
enum ExplorerSubcmd {
/// Fetch a blockchain transaction by hash
FetchTx {
/// Transaction hash
tx_hash: String,
#[arg(long)]
/// Print the full transaction information
full: bool,
#[arg(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,
#[arg(long)]
/// Encode specific history record transaction
/// to base58.
encode: bool,
},
}
#[derive(Subcommand)]
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
#[clap(short, long)]
alias: Option,
/// Token to search alias for
#[clap(short, long)]
token: Option,
},
/// Remove a Token alias
Remove {
/// Token alias to remove
alias: String,
},
}
#[derive(Subcommand)]
enum TokenSubcmd {
/// Import a mint authority secret from stdin
Import,
/// 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,
},
/// Freeze a token mint
Freeze {
/// Token ID mint to freeze
token: String,
},
}
#[derive(Subcommand)]
enum SubscribeSubcmd {
/// 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.
Blocks,
/// This subscription will listen for erroneous transactions that got
/// removed from darkfid mempool.
Transactions,
}
pub struct Drk {
pub rpc_client: RpcClient,
}
impl Drk {
async fn new(endpoint: Url) -> Result {
let rpc_client = RpcClient::new(endpoint).await?;
Ok(Self { rpc_client })
}
async fn ping(&self) -> Result<()> {
let latency = Instant::now();
let req = JsonRequest::new("ping", json!([]));
let rep = self.rpc_client.oneshot_request(req).await?;
let latency = latency.elapsed();
println!("Got reply: {}", rep);
println!("Latency: {:?}", latency);
Ok(())
}
}
#[async_std::main]
async fn main() -> Result<()> {
let args = Args::parse();
if args.verbose > 0 {
let log_level = get_log_level(args.verbose.into());
let log_config = get_log_config();
TermLogger::init(log_level, log_config, TerminalMode::Mixed, ColorChoice::Auto)?;
}
match args.command {
Subcmd::Ping => {
let drk = Drk::new(args.endpoint).await?;
drk.ping().await.with_context(|| "Failed to ping darkfid RPC endpoint")?;
Ok(())
}
Subcmd::Completions { shell } => {
let mut cmd = Args::command();
generate(shell, &mut cmd, "./drk", &mut std::io::stdout());
Ok(())
}
Subcmd::Wallet {
initialize,
keygen,
balance,
address,
secrets,
import_secrets,
tree,
coins,
} => {
if !initialize &&
!keygen &&
!balance &&
!address &&
!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.endpoint).await?;
if initialize {
drk.initialize_wallet().await?;
drk.initialize_money().await?;
drk.initialize_dao().await?;
return Ok(())
}
if keygen {
drk.money_keygen().await.with_context(|| "Failed to generate keypair")?;
return Ok(())
}
if balance {
let balmap =
drk.money_balance().await.with_context(|| "Failed to fetch wallet balance")?;
let aliases_map = drk
.get_aliases_mapped_by_token()
.await
.with_context(|| "Failed to fetch wallet aliases")?;
// 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 => "-",
};
// FIXME: Don't hardcode to 8 decimals
table.add_row(row![token_id, aliases, encode_base10(*balance, 8)]);
}
if table.is_empty() {
println!("No unspent balances found");
} else {
println!("{}", table);
}
return Ok(())
}
if address {
let address = drk
.wallet_address(1) // <-- TODO: Use is_default from the sql table
.await
.with_context(|| "Failed to fetch default address")?;
println!("{}", address);
return Ok(())
}
if secrets {
let v = drk
.get_money_secrets()
.await
.with_context(|| "Failed to fetch wallet secrets")?;
drk.rpc_client.close().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(&bytes) else {
eprintln!("Warning: Failed to deserialize secret on line {}", i);
continue
};
secrets.push(secret);
}
}
let pubkeys = drk
.import_money_secrets(secrets)
.await
.with_context(|| "Failed to import secret keys into wallet")?;
drk.rpc_client.close().await?;
for key in pubkeys {
println!("{}", key);
}
return Ok(())
}
if tree {
let v =
drk.get_money_tree().await.with_context(|| "Failed to fetch Merkle tree")?;
drk.rpc_client.close().await?;
println!("{:#?}", v);
return Ok(())
}
if coins {
let coins = drk
.get_coins(true)
.await
.with_context(|| "Failed to fetch coins from wallet")?;
let aliases_map = drk
.get_aliases_mapped_by_token()
.await
.with_context(|| "Failed to fetch wallet aliases")?;
drk.rpc_client.close().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
]);
}
println!("{}", table);
return Ok(())
}
unreachable!()
}
Subcmd::Unspend { coin } => {
let bytes: [u8; 32] = bs58::decode(&coin).into_vec()?.try_into().unwrap();
let elem: pallas::Base = match pallas::Base::from_repr(bytes).into() {
Some(v) => v,
None => return Err(anyhow!("Invalid coin")),
};
let coin = Coin::from(elem);
let drk = Drk::new(args.endpoint).await?;
drk.unspend_coin(&coin).await.with_context(|| "Failed to mark coin as unspent")?;
Ok(())
}
Subcmd::Airdrop { faucet_endpoint, amount, address } => {
let amount = f64::from_str(&amount).with_context(|| "Invalid amount")?;
let drk = Drk::new(args.endpoint).await?;
let address = match address {
Some(v) => PublicKey::from_str(v.as_str()).with_context(|| "Invalid address")?,
None => drk.wallet_address(1).await.with_context(|| {
"Failed to fetch default address, perhaps the wallet was not initialized?"
})?,
};
let txid = drk
.request_airdrop(faucet_endpoint, amount, address)
.await
.with_context(|| "Failed to request airdrop")?;
println!("Transaction ID: {}", txid);
Ok(())
}
Subcmd::Transfer { amount, token, recipient, dao, dao_bulla } => {
let _ = f64::from_str(&amount).with_context(|| "Invalid amount")?;
let rcpt = PublicKey::from_str(&recipient).with_context(|| "Invalid recipient")?;
let drk = Drk::new(args.endpoint).await?;
let token_id = drk.get_token(token).await.with_context(|| "Invalid token alias")?;
let tx = drk
.transfer(&amount, token_id, rcpt, dao, dao_bulla)
.await
.with_context(|| "Failed to create payment transaction")?;
println!("{}", bs58::encode(&serialize(&tx)).into_string());
Ok(())
}
Subcmd::Otc(cmd) => {
let drk = Drk::new(args.endpoint).await?;
match cmd {
OtcSubcmd::Init { value_pair, token_pair } => {
let (vp_send, vp_recv) = parse_value_pair(&value_pair)?;
let (tp_send, tp_recv) = parse_token_pair(&drk, &token_pair).await?;
let half = drk
.init_swap(vp_send, tp_send, vp_recv, tp_recv)
.await
.with_context(|| "Failed to create swap transaction half")?;
println!("{}", bs58::encode(&serialize(&half)).into_string());
Ok(())
}
OtcSubcmd::Join => {
let mut buf = String::new();
stdin().read_to_string(&mut buf)?;
let bytes = bs58::decode(&buf.trim()).into_vec()?;
let partial: PartialSwapData = deserialize(&bytes)?;
let tx = drk
.join_swap(partial)
.await
.with_context(|| "Failed to create a join swap transaction")?;
println!("{}", bs58::encode(&serialize(&tx)).into_string());
Ok(())
}
OtcSubcmd::Inspect => {
let mut buf = String::new();
stdin().read_to_string(&mut buf)?;
let bytes = bs58::decode(&buf.trim()).into_vec()?;
drk.inspect_swap(bytes).await.with_context(|| "Failed to inspect swap")?;
Ok(())
}
OtcSubcmd::Sign => {
let mut buf = String::new();
stdin().read_to_string(&mut buf)?;
let bytes = bs58::decode(&buf.trim()).into_vec()?;
let mut tx: Transaction = deserialize(&bytes)?;
drk.sign_swap(&mut tx)
.await
.with_context(|| "Failed to sign joined swap transaction")?;
println!("{}", bs58::encode(&serialize(&tx)).into_string());
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)?;
println!("{:#?}", 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.endpoint).await?;
let txid =
drk.broadcast_tx(&tx).await.with_context(|| "Failed to broadcast transaction")?;
println!("Transaction ID: {}", txid);
Ok(())
}
Subcmd::Subscribe(cmd) => match cmd {
SubscribeSubcmd::Blocks => {
let drk = Drk::new(args.endpoint.clone()).await?;
drk.subscribe_blocks(args.endpoint.clone())
.await
.with_context(|| "Block subscription failed")?;
Ok(())
}
SubscribeSubcmd::Transactions => {
let drk = Drk::new(args.endpoint.clone()).await?;
drk.subscribe_err_txs(args.endpoint)
.await
.with_context(|| "Erroneous transactions subscription failed")?;
Ok(())
}
},
Subcmd::Scan { reset, list, checkpoint } => {
let drk = Drk::new(args.endpoint).await?;
if reset {
eprintln!("Reset requested.");
drk.scan_blocks(true).await.with_context(|| "Failed during scanning")?;
return Ok(())
}
if list {
eprintln!("List requested.");
// TODO: implement
return Ok(())
}
if let Some(c) = checkpoint {
eprintln!("Checkpoint requested: {}", c);
// TODO: implement
return Ok(())
}
drk.scan_blocks(false).await.with_context(|| "Failed during scanning")?;
eprintln!("Finished scanning blockchain");
Ok(())
}
Subcmd::Dao(cmd) => match cmd {
DaoSubcmd::Create { proposer_limit, quorum, approval_ratio, gov_token_id } => {
let _ = f64::from_str(&proposer_limit).with_context(|| "Invalid proposer limit")?;
let _ = f64::from_str(&quorum).with_context(|| "Invalid quorum")?;
let proposer_limit = decode_base10(&proposer_limit, 8, true)?;
let quorum = decode_base10(&quorum, 8, true)?;
if approval_ratio > 1.0 {
eprintln!("Error: Approval ratio cannot be >1.0");
exit(1);
}
let approval_ratio_base = 100_u64;
let approval_ratio_quot = (approval_ratio * approval_ratio_base as f64) as u64;
let drk = Drk::new(args.endpoint).await?;
let gov_token_id =
drk.get_token(gov_token_id).await.with_context(|| "Invalid Token ID")?;
let secret_key = SecretKey::random(&mut OsRng);
let bulla_blind = pallas::Base::random(&mut OsRng);
let dao_params = DaoParams {
proposer_limit,
quorum,
approval_ratio_base,
approval_ratio_quot,
gov_token_id,
secret_key,
bulla_blind,
};
let encoded = bs58::encode(&serialize(&dao_params)).into_string();
println!("{}", encoded);
Ok(())
}
DaoSubcmd::View => {
let mut buf = String::new();
stdin().read_to_string(&mut buf)?;
let bytes = bs58::decode(&buf.trim()).into_vec()?;
let dao_params: DaoParams = deserialize(&bytes)?;
println!("{}", dao_params);
Ok(())
}
DaoSubcmd::Import { dao_name } => {
let mut buf = String::new();
stdin().read_to_string(&mut buf)?;
let bytes = bs58::decode(&buf.trim()).into_vec()?;
let dao_params: DaoParams = deserialize(&bytes)?;
let drk = Drk::new(args.endpoint).await?;
drk.import_dao(dao_name, dao_params)
.await
.with_context(|| "Failed to import DAO")?;
Ok(())
}
DaoSubcmd::List { dao_id } => {
let drk = Drk::new(args.endpoint).await?;
drk.dao_list(dao_id).await.with_context(|| "Failed to list DAO")?;
Ok(())
}
DaoSubcmd::Balance { dao_alias } => {
let drk = Drk::new(args.endpoint).await?;
let dao_id = drk.get_dao_id(&dao_alias).await?;
let balmap =
drk.dao_balance(dao_id).await.with_context(|| "Failed to fetch DAO balance")?;
let aliases_map = drk
.get_aliases_mapped_by_token()
.await
.with_context(|| "Failed to fetch wallet aliases")?;
// 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 => "-",
};
// FIXME: Don't hardcode to 8 decimals
table.add_row(row![token_id, aliases, encode_base10(*balance, 8)]);
}
if table.is_empty() {
println!("No unspent balances found");
} else {
println!("{}", table);
}
Ok(())
}
DaoSubcmd::Mint { dao_alias } => {
let drk = Drk::new(args.endpoint).await?;
let dao_id = drk.get_dao_id(&dao_alias).await?;
let tx = drk.dao_mint(dao_id).await.with_context(|| "Failed to mint DAO")?;
println!("{}", bs58::encode(&serialize(&tx)).into_string());
Ok(())
}
DaoSubcmd::Propose { dao_alias, recipient, amount, token } => {
let _ = f64::from_str(&amount).with_context(|| "Invalid amount")?;
let amount = decode_base10(&amount, 8, true)?;
let rcpt = PublicKey::from_str(&recipient).with_context(|| "Invalid recipient")?;
let drk = Drk::new(args.endpoint).await?;
let dao_id = drk.get_dao_id(&dao_alias).await?;
let token_id = drk.get_token(token).await.with_context(|| "Invalid token alias")?;
let tx = drk
.dao_propose(dao_id, rcpt, amount, token_id)
.await
.with_context(|| "Failed to create DAO proposal")?;
println!("{}", bs58::encode(&serialize(&tx)).into_string());
Ok(())
}
DaoSubcmd::Proposals { dao_alias } => {
let drk = Drk::new(args.endpoint).await?;
let dao_id = drk.get_dao_id(&dao_alias).await?;
let proposals = drk.get_dao_proposals(dao_id).await?;
for proposal in proposals {
println!("[{}] {:?}", proposal.id, proposal.bulla());
}
Ok(())
}
DaoSubcmd::Proposal { dao_alias, proposal_id } => {
let drk = Drk::new(args.endpoint).await?;
let dao_id = drk.get_dao_id(&dao_alias).await?;
let proposals = drk.get_dao_proposals(dao_id).await?;
let Some(proposal) = proposals.iter().find(|x| x.id == proposal_id) else {
eprintln!("No such DAO proposal found");
exit(1);
};
println!("{}", proposal);
Ok(())
}
DaoSubcmd::Vote { dao_alias, proposal_id, vote, vote_weight } => {
let drk = Drk::new(args.endpoint).await?;
let dao_id = drk.get_dao_id(&dao_alias).await?;
let _ = f64::from_str(&vote_weight).with_context(|| "Invalid vote weight")?;
let weight = decode_base10(&vote_weight, 8, true)?;
if vote > 1 {
eprintln!("Vote can be either 0 (NO) or 1 (YES)");
exit(1);
}
let vote = vote != 0;
let tx = drk
.dao_vote(dao_id, proposal_id, vote, weight)
.await
.with_context(|| "Failed to create DAO Vote transaction")?;
// TODO: Write our_vote in the proposal sql.
println!("{}", bs58::encode(&serialize(&tx)).into_string());
Ok(())
}
DaoSubcmd::Exec { dao_alias, proposal_id } => {
let drk = Drk::new(args.endpoint).await?;
let dao_id = drk.get_dao_id(&dao_alias).await?;
let dao = drk.get_dao_by_id(dao_id).await?;
let proposal = drk.get_dao_proposal_by_id(proposal_id).await?;
assert!(proposal.dao_bulla == dao.bulla());
let tx = drk
.dao_exec(dao, proposal)
.await
.with_context(|| "Failed to execute DAO proposal")?;
println!("{}", bs58::encode(&serialize(&tx)).into_string());
Ok(())
}
},
Subcmd::Explorer(cmd) => match cmd {
ExplorerSubcmd::FetchTx { tx_hash, full, encode } => {
let tx_hash = blake3::Hash::from_hex(&tx_hash)?;
let drk = Drk::new(args.endpoint).await?;
let tx = if let Some(tx) =
drk.get_tx(&tx_hash).await.with_context(|| "Failed to fetch transaction")?
{
tx
} else {
eprintln!("Transaction was not found");
exit(1);
};
// Make sure the tx is correct
assert_eq!(tx.hash(), tx_hash);
if encode {
println!("{}", bs58::encode(&serialize(&tx)).into_string());
exit(1)
}
println!("Transaction ID: {}", tx_hash);
if full {
println!("{:?}", 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.endpoint).await?;
let is_valid =
drk.simulate_tx(&tx).await.with_context(|| "Failed to simulate tx")?;
println!("Transaction ID: {}", tx.hash());
println!("State: {}", if is_valid { "valid" } else { "invalid" });
Ok(())
}
ExplorerSubcmd::TxsHistory { tx_hash, encode } => {
let drk = Drk::new(args.endpoint).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)
}
println!("Transaction ID: {}", tx_hash);
println!("Status: {}", status);
println!("{:?}", tx);
return Ok(())
}
let map = drk.get_txs_history().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!["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(())
}
},
Subcmd::Alias(cmd) => match cmd {
AliasSubcmd::Add { alias, token } => {
if alias.chars().count() > 5 {
eprintln!("Error: Alias exceeds 5 characters");
exit(1);
}
let token_id =
TokenId::try_from(token.as_str()).with_context(|| "Invalid Token ID")?;
let drk = Drk::new(args.endpoint).await?;
drk.add_alias(alias, token_id).await?;
Ok(())
}
AliasSubcmd::Show { alias, token } => {
let token_id = match token {
Some(t) => {
Some(TokenId::try_from(t.as_str()).with_context(|| "Invalid Token ID")?)
}
None => None,
};
let drk = Drk::new(args.endpoint).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 = Drk::new(args.endpoint).await?;
drk.remove_alias(alias).await?;
Ok(())
}
},
Subcmd::Token(cmd) => match cmd {
TokenSubcmd::Import => {
let mut buf = String::new();
stdin().read_to_string(&mut buf)?;
let mint_authority =
SecretKey::from_str(buf.trim()).with_context(|| "Invalid secret key")?;
let drk = Drk::new(args.endpoint).await?;
drk.import_mint_authority(mint_authority).await?;
let token_id = TokenId::derive(mint_authority);
eprintln!("Successfully imported mint authority for token ID: {}", token_id);
Ok(())
}
TokenSubcmd::GenerateMint => {
let mint_authority = SecretKey::random(&mut OsRng);
let drk = Drk::new(args.endpoint).await?;
drk.import_mint_authority(mint_authority).await?;
let token_id = TokenId::derive(mint_authority);
eprintln!("Successfully imported mint authority for token ID: {}", token_id);
Ok(())
}
TokenSubcmd::List => {
let drk = Drk::new(args.endpoint).await?;
let tokens = drk.list_tokens().await?;
let aliases_map = drk
.get_aliases_mapped_by_token()
.await
.with_context(|| "Failed to fetch wallet aliases")?;
let mut table = Table::new();
table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
table.set_titles(row!["Token ID", "Aliases", "Mint Authority", "Frozen"]);
for (token_id, authority, 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, frozen]);
}
if table.is_empty() {
println!("No tokens found");
} else {
println!("{}", table);
}
Ok(())
}
// TODO: Mint directly into DAO treasury
TokenSubcmd::Mint { token, amount, recipient } => {
let drk = Drk::new(args.endpoint).await?;
let _ = f64::from_str(&amount).with_context(|| "Invalid amount")?;
let rcpt = PublicKey::from_str(&recipient).with_context(|| "Invalid recipient")?;
let token_id = drk.get_token(token).await.with_context(|| "Invalid Token ID")?;
let tx = drk
.mint_token(&amount, rcpt, token_id)
.await
.with_context(|| "Failed to create token mint transaction")?;
println!("{}", bs58::encode(&serialize(&tx)).into_string());
Ok(())
}
TokenSubcmd::Freeze { token } => {
let drk = Drk::new(args.endpoint).await?;
let token_id = drk.get_token(token).await.with_context(|| "Invalid Token ID")?;
let tx = drk
.freeze_token(token_id)
.await
.with_context(|| "Failed to create token freeze transaction")?;
println!("{}", bs58::encode(&serialize(&tx)).into_string());
Ok(())
}
},
}
}