diff --git a/bin/drk2/src/cli_util.rs b/bin/drk2/src/cli_util.rs index 951ff5470..aaf2fcc2e 100644 --- a/bin/drk2/src/cli_util.rs +++ b/bin/drk2/src/cli_util.rs @@ -120,16 +120,16 @@ pub fn generate_completions(shell: &str) -> Result<()> { let addresses = Arg::with_name("addresses").long("addresses").help("Print all the addresses in the wallet"); - let default_address = Arg::with_name("default_address") - .long("default_address") + let default_address = Arg::with_name("default-address") + .long("default-address") .takes_value(true) .help("Set the default address in the wallet"); let secrets = Arg::with_name("secrets").long("secrets").help("Print all the secret keys from the wallet"); - let import_secrets = Arg::with_name("import_secrets") - .long("import_secrets") + let import_secrets = Arg::with_name("import-secrets") + .long("import-secrets") .help("Import secret keys from stdin into the wallet, separated by newlines"); let tree = Arg::with_name("tree").long("tree").help("Print the Merkle tree in the wallet"); @@ -166,15 +166,15 @@ pub fn generate_completions(shell: &str) -> Result<()> { .args(&vec![amount, token, recipient]); // Otc - let value_pair = Arg::with_name("value_pair") + let value_pair = Arg::with_name("value-pair") .short("v") - .long("value_pair") + .long("value-pair") .takes_value(true) .help("Value pair to send:recv (11.55:99.42)"); - let token_pair = Arg::with_name("token_pair") + let token_pair = Arg::with_name("token-pair") .short("t") - .long("token_pair") + .long("token-pair") .takes_value(true) .help("Token pair to send:recv (f00:b4r)"); @@ -210,7 +210,88 @@ pub fn generate_completions(shell: &str) -> Result<()> { find coins sent to us and fill our wallet with the necessary metadata.", ); - // TODO: DAO + // 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 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(&vec![ + proposer_limit, + quorum, + approval_ratio, + gov_token_id, + ]); + + let view = SubCommand::with_name("view").about("View DAO data from stdin"); + + let dao_name = Arg::with_name("dao-name").help("Named identifier for the DAO"); + + let import = + SubCommand::with_name("import").about("Import DAO data from stdin").args(&vec![dao_name]); + + let dao_alias = Arg::with_name("dao-alias").help("Numeric identifier for the DAO (optional)"); + + let list = SubCommand::with_name("list") + .about("List imported DAOs (or info about a specific one)") + .args(&vec![dao_alias]); + + let dao_alias = Arg::with_name("dao-alias").help("Name or numeric identifier for the DAO"); + + let balance = SubCommand::with_name("balance") + .about("Show the balance of a DAO") + .args(&vec![dao_alias.clone()]); + + let mint = SubCommand::with_name("mint") + .about("Mint an imported DAO on-chain") + .args(&vec![dao_alias.clone()]); + + let recipient = + Arg::with_name("recipient").help("Pubkey to send tokens to with proposal success"); + + let amount = Arg::with_name("amount").help("Amount to send from DAO with proposal success"); + + let token = Arg::with_name("token").help("Token ID to send from DAO with proposal success"); + + let propose = SubCommand::with_name("propose") + .about("Create a proposal for a DAO") + .args(&vec![dao_alias.clone(), recipient, amount, token]); + + let proposals = SubCommand::with_name("proposals") + .about("List DAO proposals") + .args(&vec![dao_alias.clone()]); + + let proposal_id = Arg::with_name("proposal-id").help("Numeric identifier for the proposal"); + + let proposal = SubCommand::with_name("proposal") + .about("View a DAO proposal data") + .args(&vec![dao_alias.clone(), proposal_id.clone()]); + + let vote = Arg::with_name("vote").help("Vote (0 for NO, 1 for YES)"); + + let vote_weight = + Arg::with_name("vote-weight").help("Vote weight (amount of governance tokens)"); + + let vote = SubCommand::with_name("vote").about("Vote on a given proposal").args(&vec![ + dao_alias.clone(), + proposal_id.clone(), + vote, + vote_weight, + ]); + + let exec = SubCommand::with_name("exec") + .about("Execute a DAO proposal") + .args(&vec![dao_alias, proposal_id]); + + let dao = SubCommand::with_name("dao").about("DAO functionalities").subcommands(vec![ + create, view, import, list, balance, mint, propose, proposals, proposal, vote, exec, + ]); // Scan let reset = Arg::with_name("reset") @@ -354,6 +435,7 @@ pub fn generate_completions(shell: &str) -> Result<()> { inspect, broadcast, subscribe, + dao, scan, explorer, alias, diff --git a/bin/drk2/src/dao.rs b/bin/drk2/src/dao.rs index 7c949e8f8..2e7eb967d 100644 --- a/bin/drk2/src/dao.rs +++ b/bin/drk2/src/dao.rs @@ -16,36 +16,55 @@ * along with this program. If not, see . */ -use std::fmt; +use std::{collections::HashMap, fmt}; +use rand::rngs::OsRng; use rusqlite::types::Value; -use darkfi::{tx::Transaction, util::parse::encode_base10, Error, Result}; +use darkfi::{ + tx::{ContractCallLeaf, Transaction, TransactionBuilder}, + util::parse::encode_base10, + zk::{empty_witnesses, halo2::Field, ProvingKey, ZkCircuit}, + zkas::ZkBinary, + Error, Result, +}; use darkfi_dao_contract::{ client::{ - DaoVoteNote, DAO_DAOS_COL_CALL_INDEX, DAO_DAOS_COL_DAO_ID, DAO_DAOS_COL_LEAF_POSITION, + make_mint_call, DaoProposeCall, DaoProposeStakeInput, DaoVoteCall, DaoVoteInput, + DAO_DAOS_COL_APPROVAL_RATIO_BASE, DAO_DAOS_COL_APPROVAL_RATIO_QUOT, + DAO_DAOS_COL_BULLA_BLIND, DAO_DAOS_COL_CALL_INDEX, DAO_DAOS_COL_DAO_ID, + DAO_DAOS_COL_GOV_TOKEN_ID, DAO_DAOS_COL_LEAF_POSITION, DAO_DAOS_COL_NAME, + DAO_DAOS_COL_PROPOSER_LIMIT, DAO_DAOS_COL_QUORUM, DAO_DAOS_COL_SECRET, DAO_DAOS_COL_TX_HASH, DAO_DAOS_TABLE, DAO_PROPOSALS_COL_AMOUNT, DAO_PROPOSALS_COL_BULLA_BLIND, DAO_PROPOSALS_COL_CALL_INDEX, DAO_PROPOSALS_COL_DAO_ID, DAO_PROPOSALS_COL_LEAF_POSITION, DAO_PROPOSALS_COL_MONEY_SNAPSHOT_TREE, - DAO_PROPOSALS_COL_RECV_PUBLIC, DAO_PROPOSALS_COL_SENDCOIN_TOKEN_ID, - DAO_PROPOSALS_COL_TX_HASH, DAO_PROPOSALS_TABLE, DAO_TREES_COL_DAOS_TREE, - DAO_TREES_COL_PROPOSALS_TREE, DAO_TREES_TABLE, DAO_VOTES_COL_ALL_VOTE_BLIND, - DAO_VOTES_COL_ALL_VOTE_VALUE, DAO_VOTES_COL_CALL_INDEX, DAO_VOTES_COL_PROPOSAL_ID, - DAO_VOTES_COL_TX_HASH, DAO_VOTES_COL_VOTE_OPTION, DAO_VOTES_COL_YES_VOTE_BLIND, - DAO_VOTES_TABLE, + DAO_PROPOSALS_COL_PROPOSAL_ID, DAO_PROPOSALS_COL_RECV_PUBLIC, + DAO_PROPOSALS_COL_SENDCOIN_TOKEN_ID, DAO_PROPOSALS_COL_TX_HASH, DAO_PROPOSALS_TABLE, + DAO_TREES_COL_DAOS_TREE, DAO_TREES_COL_PROPOSALS_TREE, DAO_TREES_TABLE, + DAO_VOTES_COL_ALL_VOTE_BLIND, DAO_VOTES_COL_ALL_VOTE_VALUE, DAO_VOTES_COL_CALL_INDEX, + DAO_VOTES_COL_PROPOSAL_ID, DAO_VOTES_COL_TX_HASH, DAO_VOTES_COL_VOTE_OPTION, + DAO_VOTES_COL_YES_VOTE_BLIND, DAO_VOTES_TABLE, }, - model::{DaoBulla, DaoMintParams, DaoProposeParams, DaoVoteParams}, - DaoFunction, + model::{DaoAuthCall, DaoBulla, DaoMintParams, DaoProposeParams, DaoVoteParams}, + DaoFunction, DAO_CONTRACT_ZKAS_DAO_MINT_NS, DAO_CONTRACT_ZKAS_DAO_PROPOSE_INPUT_NS, + DAO_CONTRACT_ZKAS_DAO_PROPOSE_MAIN_NS, DAO_CONTRACT_ZKAS_DAO_VOTE_INPUT_NS, + DAO_CONTRACT_ZKAS_DAO_VOTE_MAIN_NS, }; +use darkfi_money_contract::{client::OwnCoin, MoneyFunction}; use darkfi_sdk::{ bridgetree, crypto::{ - poseidon_hash, Keypair, MerkleNode, MerkleTree, PublicKey, SecretKey, TokenId, - DAO_CONTRACT_ID, + poseidon_hash, + util::{fp_mod_fv, fp_to_u64}, + Keypair, MerkleNode, MerkleTree, PublicKey, SecretKey, TokenId, DAO_CONTRACT_ID, + MONEY_CONTRACT_ID, }, pasta::pallas, + ContractCall, +}; +use darkfi_serial::{ + async_trait, deserialize, serialize, Encodable, SerialDecodable, SerialEncodable, }; -use darkfi_serial::{async_trait, deserialize, serialize, SerialDecodable, SerialEncodable}; use crate::{ convert_named_params, @@ -67,6 +86,52 @@ pub struct DaoProposeNote { pub proposal: DaoProposalInfo, } +#[derive(Debug, Clone, SerialEncodable, SerialDecodable)] +/// Parameters representing a DAO to be initialized +pub struct DaoParams { + /// The minimum amount of governance tokens needed to open a proposal + pub proposer_limit: u64, + /// Minimal threshold of participating total tokens needed for a proposal to pass + pub quorum: u64, + /// The ratio of winning/total votes needed for a proposal to pass + pub approval_ratio_base: u64, + pub approval_ratio_quot: u64, + /// DAO's governance token ID + pub gov_token_id: TokenId, + /// Secret key for the DAO + pub secret_key: SecretKey, + /// DAO bulla blind + pub bulla_blind: pallas::Base, +} + +impl fmt::Display for DaoParams { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let s = format!( + "{}\n{}\n{}: {} ({})\n{}: {} ({})\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {:?}", + "DAO Parameters", + "==============", + "Proposer limit", + encode_base10(self.proposer_limit, 8), + self.proposer_limit, + "Quorum", + encode_base10(self.quorum, 8), + self.quorum, + "Approval ratio", + self.approval_ratio_quot as f64 / self.approval_ratio_base as f64, + "Governance Token ID", + self.gov_token_id, + "Public key", + PublicKey::from_secret(self.secret_key), + "Secret key", + self.secret_key, + "Bulla blind", + self.bulla_blind, + ); + + write!(f, "{}", s) + } +} + #[derive(Debug, Clone)] /// Parameters representing an intialized DAO, optionally deployed on-chain pub struct Dao { @@ -293,7 +358,7 @@ impl Drk { .await } - /// Fetch DAO Merkle trees from the wallet + /// Fetch DAO Merkle trees from the wallet. pub async fn get_dao_trees(&self) -> Result<(MerkleTree, MerkleTree)> { let row = match self.wallet.query_single(DAO_TREES_TABLE, &[], &[]).await { Ok(r) => r, @@ -317,7 +382,7 @@ impl Drk { Ok((daos_tree, proposals_tree)) } - /// Fetch all DAO secret keys from the wallet + /// Fetch all DAO secret keys from the wallet. pub async fn get_dao_secrets(&self) -> Result> { let daos = self.get_daos().await?; let mut ret = Vec::with_capacity(daos.len()); @@ -437,7 +502,104 @@ impl Drk { Ok(daos) } - /// Fetch all known DAO proposals from the wallet given a DAO ID + /// Auxiliary function to parse a proposal record row. + fn parse_dao_proposal(&self, dao: &Dao, row: &[Value]) -> Result { + let Value::Integer(id) = row[0] else { + return Err(Error::ParseFailed("[get_dao_proposals] ID parsing failed")) + }; + let Ok(id) = u64::try_from(id) else { + return Err(Error::ParseFailed("[get_dao_proposals] ID parsing failed")) + }; + + let Value::Integer(dao_id) = row[1] else { + return Err(Error::ParseFailed("[get_dao_proposals] DAO ID parsing failed")) + }; + let Ok(dao_id) = u64::try_from(dao_id) else { + return Err(Error::ParseFailed("[get_dao_proposals] DAO ID parsing failed")) + }; + assert!(dao_id == dao.id); + let dao_bulla = dao.bulla(); + + let Value::Blob(ref recipient_bytes) = row[2] else { + return Err(Error::ParseFailed( + "[get_dao_proposals] Recipient bytes bytes parsing failed", + )) + }; + let recipient = deserialize(recipient_bytes)?; + + let Value::Blob(ref amount_bytes) = row[3] else { + return Err(Error::ParseFailed("[get_dao_proposals] Amount bytes parsing failed")) + }; + let amount = deserialize(amount_bytes)?; + + let Value::Blob(ref token_id_bytes) = row[4] else { + return Err(Error::ParseFailed("[get_dao_proposals] Token ID bytes parsing failed")) + }; + let token_id = deserialize(token_id_bytes)?; + + let Value::Blob(ref bulla_blind_bytes) = row[5] else { + return Err(Error::ParseFailed("[get_dao_proposals] Bulla blind bytes parsing failed")) + }; + let bulla_blind = deserialize(bulla_blind_bytes)?; + + let Value::Blob(ref leaf_position_bytes) = row[6] else { + return Err(Error::ParseFailed("[get_dao_proposals] Leaf position bytes parsing failed")) + }; + let leaf_position = if leaf_position_bytes.is_empty() { + None + } else { + Some(deserialize(leaf_position_bytes)?) + }; + + let Value::Blob(ref money_snapshot_tree_bytes) = row[7] else { + return Err(Error::ParseFailed( + "[get_dao_proposals] Money snapshot tree bytes parsing failed", + )) + }; + let money_snapshot_tree = if money_snapshot_tree_bytes.is_empty() { + None + } else { + Some(deserialize(money_snapshot_tree_bytes)?) + }; + + let Value::Blob(ref tx_hash_bytes) = row[8] else { + return Err(Error::ParseFailed( + "[get_dao_proposals] Transaction hash bytes parsing failed", + )) + }; + let tx_hash = + if tx_hash_bytes.is_empty() { None } else { Some(deserialize(tx_hash_bytes)?) }; + + let Value::Integer(call_index) = row[9] else { + return Err(Error::ParseFailed("[get_dao_proposals] Call index parsing failed")) + }; + let Ok(call_index) = u32::try_from(call_index) else { + return Err(Error::ParseFailed("[get_dao_proposals] Call index parsing failed")) + }; + let call_index = Some(call_index); + + let Value::Blob(ref vote_id_bytes) = row[10] else { + return Err(Error::ParseFailed("[get_dao_proposals] Vote ID bytes parsing failed")) + }; + let vote_id = + if vote_id_bytes.is_empty() { None } else { Some(deserialize(vote_id_bytes)?) }; + + Ok(DaoProposal { + id, + dao_bulla, + recipient, + amount, + token_id, + bulla_blind, + leaf_position, + money_snapshot_tree, + tx_hash, + call_index, + vote_id, + }) + } + + /// Fetch all known DAO proposals from the wallet given a DAO ID. pub async fn get_dao_proposals(&self, dao_id: u64) -> Result> { let daos = self.get_daos().await?; let Some(dao) = daos.get(dao_id as usize - 1) else { @@ -465,104 +627,7 @@ impl Drk { let mut proposals = Vec::with_capacity(rows.len()); for row in rows { - let Value::Integer(id) = row[0] else { - return Err(Error::ParseFailed("[get_dao_proposals] ID parsing failed")) - }; - let Ok(id) = u64::try_from(id) else { - return Err(Error::ParseFailed("[get_dao_proposals] ID parsing failed")) - }; - - let Value::Integer(dao_id) = row[1] else { - return Err(Error::ParseFailed("[get_dao_proposals] DAO ID parsing failed")) - }; - let Ok(dao_id) = u64::try_from(dao_id) else { - return Err(Error::ParseFailed("[get_dao_proposals] DAO ID parsing failed")) - }; - assert!(dao_id == dao.id); - let dao_bulla = dao.bulla(); - - let Value::Blob(ref recipient_bytes) = row[2] else { - return Err(Error::ParseFailed( - "[get_dao_proposals] Recipient bytes bytes parsing failed", - )) - }; - let recipient = deserialize(recipient_bytes)?; - - let Value::Blob(ref amount_bytes) = row[3] else { - return Err(Error::ParseFailed("[get_dao_proposals] Amount bytes parsing failed")) - }; - let amount = deserialize(amount_bytes)?; - - let Value::Blob(ref token_id_bytes) = row[4] else { - return Err(Error::ParseFailed("[get_dao_proposals] Token ID bytes parsing failed")) - }; - let token_id = deserialize(token_id_bytes)?; - - let Value::Blob(ref bulla_blind_bytes) = row[5] else { - return Err(Error::ParseFailed( - "[get_dao_proposals] Bulla blind bytes parsing failed", - )) - }; - let bulla_blind = deserialize(bulla_blind_bytes)?; - - let Value::Blob(ref leaf_position_bytes) = row[6] else { - return Err(Error::ParseFailed( - "[get_dao_proposals] Leaf position bytes parsing failed", - )) - }; - let leaf_position = if leaf_position_bytes.is_empty() { - None - } else { - Some(deserialize(leaf_position_bytes)?) - }; - - let Value::Blob(ref money_snapshot_tree_bytes) = row[7] else { - return Err(Error::ParseFailed( - "[get_dao_proposals] Money snapshot tree bytes parsing failed", - )) - }; - let money_snapshot_tree = if money_snapshot_tree_bytes.is_empty() { - None - } else { - Some(deserialize(money_snapshot_tree_bytes)?) - }; - - let Value::Blob(ref tx_hash_bytes) = row[8] else { - return Err(Error::ParseFailed( - "[get_dao_proposals] Transaction hash bytes parsing failed", - )) - }; - let tx_hash = - if tx_hash_bytes.is_empty() { None } else { Some(deserialize(tx_hash_bytes)?) }; - - let Value::Integer(call_index) = row[9] else { - return Err(Error::ParseFailed("[get_dao_proposals] Call index parsing failed")) - }; - let Ok(call_index) = u32::try_from(call_index) else { - return Err(Error::ParseFailed("[get_dao_proposals] Call index parsing failed")) - }; - let call_index = Some(call_index); - - let Value::Blob(ref vote_id_bytes) = row[10] else { - return Err(Error::ParseFailed("[get_dao_proposals] Vote ID bytes parsing failed")) - }; - let vote_id = - if vote_id_bytes.is_empty() { None } else { Some(deserialize(vote_id_bytes)?) }; - - let proposal = DaoProposal { - id, - dao_bulla, - recipient, - amount, - token_id, - bulla_blind, - leaf_position, - money_snapshot_tree, - tx_hash, - call_index, - vote_id, - }; - + let proposal = self.parse_dao_proposal(dao, &row)?; proposals.push(proposal); } @@ -687,37 +752,43 @@ impl Drk { for vote in new_dao_votes { for dao in &daos { - // TODO: we are using note_old here - if let Ok(note) = vote.0.note_old.decrypt::(&dao.secret_key) { - eprintln!("Managed to decrypt DAO proposal vote note"); - let daos_proposals = self.get_dao_proposals(dao.id).await?; - let mut proposal_id = None; + // TODO: we shouldn't decrypt with all DAOs here + let note = vote.0.note.decrypt(&dao.secret_key); + eprintln!("Managed to decrypt DAO proposal vote note"); + let daos_proposals = self.get_dao_proposals(dao.id).await?; + let mut proposal_id = None; - for i in daos_proposals { - if i.bulla() == vote.0.proposal_bulla.inner() { - proposal_id = Some(i.id); - break - } - } - - if proposal_id.is_none() { - eprintln!("Warning: Decrypted DaoVoteNote but did not find proposal"); + for i in daos_proposals { + if i.bulla() == vote.0.proposal_bulla.inner() { + proposal_id = Some(i.id); break } - - let v = DaoVote { - id: 0, - proposal_id: proposal_id.unwrap(), - vote_option: note.vote_option, - yes_vote_blind: note.yes_vote_blind, - all_vote_value: note.all_vote_value, - all_vote_blind: note.all_vote_blind, - tx_hash: vote.1, - call_index: Some(vote.2), - }; - - dao_votes.push(v); } + + if proposal_id.is_none() { + eprintln!("Warning: Decrypted DaoVoteNote but did not find proposal"); + break + } + + let vote_option = fp_to_u64(note[0]).unwrap(); + assert!(vote_option == 0 || vote_option == 1); + let vote_option = vote_option != 0; + let yes_vote_blind = fp_mod_fv(note[1]); + let all_vote_value = fp_to_u64(note[2]).unwrap(); + let all_vote_blind = fp_mod_fv(note[3]); + + let v = DaoVote { + id: 0, + proposal_id: proposal_id.unwrap(), + vote_option, + yes_vote_blind, + all_vote_value, + all_vote_blind, + tx_hash: vote.1, + call_index: Some(vote.2), + }; + + dao_votes.push(v); } } } @@ -793,7 +864,7 @@ impl Drk { Ok(()) } - /// Import given DAO proposals into the wallet + /// Import given DAO proposals into the wallet. pub async fn put_dao_proposals(&self, proposals: &[DaoProposal]) -> Result<()> { let daos = self.get_daos().await?; @@ -845,7 +916,7 @@ impl Drk { Ok(()) } - /// Import given DAO votes into the wallet + /// Import given DAO votes into the wallet. pub async fn put_dao_votes(&self, votes: &[DaoVote]) -> WalletDbResult<()> { for vote in votes { eprintln!("Importing DAO vote into wallet"); @@ -883,7 +954,7 @@ impl Drk { Ok(()) } - /// Reset the DAO Merkle trees in the wallet + /// Reset the DAO Merkle trees in the wallet. pub async fn reset_dao_trees(&self) -> WalletDbResult<()> { eprintln!("Resetting DAO Merkle trees"); let tree = MerkleTree::new(100); @@ -893,7 +964,7 @@ impl Drk { Ok(()) } - /// Reset confirmed DAOs in the wallet + /// Reset confirmed DAOs in the wallet. pub async fn reset_daos(&self) -> WalletDbResult<()> { eprintln!("Resetting DAO confirmations"); let daos = match self.get_daos().await { @@ -909,17 +980,704 @@ impl Drk { Ok(()) } - /// Reset all DAO proposals in the wallet + /// Reset all DAO proposals in the wallet. pub async fn reset_dao_proposals(&self) -> WalletDbResult<()> { eprintln!("Resetting DAO proposals"); let query = format!("DELETE FROM {};", DAO_PROPOSALS_TABLE); self.wallet.exec_sql(&query, &[]).await } - /// Reset all DAO votes in the wallet + /// Reset all DAO votes in the wallet. pub async fn reset_dao_votes(&self) -> WalletDbResult<()> { eprintln!("Resetting DAO votes"); let query = format!("DELETE FROM {};", DAO_VOTES_TABLE); self.wallet.exec_sql(&query, &[]).await } + + /// Import given DAO params into the wallet with a given name. + pub async fn import_dao(&self, dao_name: String, dao_params: DaoParams) -> Result<()> { + // First let's check if we've imported this DAO with the given name before. + // TODO: instead of getting all DAOs and filtering in rust, + // we can use the DB api directly to query for the record + // and return the error if it exists + let daos = self.get_daos().await?; + if daos.iter().any(|x| x.name == dao_name) { + return Err(Error::RusqliteError( + "[import_dao] This DAO has already been imported".to_string(), + )) + } + + eprintln!("Importing \"{dao_name}\" DAO into the wallet"); + + let query = format!( + "INSERT INTO {} ({}, {}, {}, {}, {}, {}, {}, {}) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8);", + DAO_DAOS_TABLE, + DAO_DAOS_COL_NAME, + DAO_DAOS_COL_PROPOSER_LIMIT, + DAO_DAOS_COL_QUORUM, + DAO_DAOS_COL_APPROVAL_RATIO_BASE, + DAO_DAOS_COL_APPROVAL_RATIO_QUOT, + DAO_DAOS_COL_GOV_TOKEN_ID, + DAO_DAOS_COL_SECRET, + DAO_DAOS_COL_BULLA_BLIND, + ); + if let Err(e) = self + .wallet + .exec_sql( + &query, + rusqlite::params![ + dao_name, + serialize(&dao_params.proposer_limit), + serialize(&dao_params.quorum), + dao_params.approval_ratio_base, + dao_params.approval_ratio_quot, + serialize(&dao_params.gov_token_id), + serialize(&dao_params.secret_key), + serialize(&dao_params.bulla_blind), + ], + ) + .await + { + return Err(Error::RusqliteError(format!("[import_dao] DAO insert failed: {e:?}"))) + }; + + Ok(()) + } + + /// Retrieve DAO ID using provided alias filter. + pub async fn get_dao_id_by_alias(&self, alias_filter: &str) -> Result { + let row = match self + .wallet + .query_single( + DAO_DAOS_TABLE, + &[DAO_DAOS_COL_DAO_ID], + convert_named_params! {(DAO_DAOS_COL_NAME, alias_filter)}, + ) + .await + { + Ok(r) => r, + Err(e) => { + return Err(Error::RusqliteError(format!( + "[get_dao_id_by_alias] DAO retrieval failed: {e:?}" + ))) + } + }; + + let Value::Integer(dao_id) = row[0] else { + return Err(Error::ParseFailed("[get_dao_id_by_alias] Key ID parsing failed")) + }; + let Ok(dao_id) = u64::try_from(dao_id) else { + return Err(Error::ParseFailed("[get_dao_id_by_alias] Key ID parsing failed")) + }; + + Ok(dao_id) + } + + /// Convenience function. Interprets the alias either as the DAO alias or its ID. + pub async fn get_dao_id(&self, alias: &str) -> Result { + if let Ok(id) = self.get_dao_id_by_alias(alias).await { + return Ok(id) + } + Ok(alias.parse()?) + } + + /// Fetch a DAO given a numeric ID. + pub async fn get_dao_by_id(&self, dao_id: u64) -> Result { + // TODO: instead of getting all DAOs and filtering in rust, + // we can use the DB api directly to query for the record + // and then parse it + let daos = self.get_daos().await?; + + let Some(dao) = daos.iter().find(|x| x.id == dao_id) else { + return Err(Error::RusqliteError("[get_dao_by_id] DAO not found in wallet".to_string())) + }; + + Ok(dao.clone()) + } + + /// List DAO(s) imported in the wallet. If an ID is given, just print the + /// metadata for that specific one, if found. + pub async fn dao_list(&self, dao_id: Option) -> Result<()> { + if let Some(dao_id) = dao_id { + return self.dao_list_single(dao_id).await + } + + let daos = self.get_daos().await?; + for dao in daos { + eprintln!("[{}] {}", dao.id, dao.name); + } + + Ok(()) + } + + /// Retrieve DAO for provided ID and print its metadata. + async fn dao_list_single(&self, dao_id: u64) -> Result<()> { + let dao = self.get_dao_by_id(dao_id).await?; + + eprintln!("{dao}"); + + Ok(()) + } + + /// Fetch known unspent balances from the wallet for the given DAO ID + pub async fn dao_balance(&self, dao_id: u64) -> Result> { + // TODO: instead of getting all DAOs and filtering in rust, + // we can use the DB api directly to query for the record + // and then parse it + let daos = self.get_daos().await?; + let Some(dao) = daos.get(dao_id as usize - 1) else { + return Err(Error::RusqliteError(format!("DAO with ID {dao_id} not found in wallet"))) + }; + + let mut coins = self.get_coins(false).await?; + coins.retain(|x| x.0.note.spend_hook == DAO_CONTRACT_ID.inner()); + coins.retain(|x| x.0.note.user_data == dao.bulla().inner()); + + // Fill this map with balances + let mut balmap: HashMap = HashMap::new(); + + for coin in coins { + let mut value = coin.0.note.value; + + if let Some(prev) = balmap.get(&coin.0.note.token_id.to_string()) { + value += prev; + } + + balmap.insert(coin.0.note.token_id.to_string(), value); + } + + Ok(balmap) + } + + /// Fetch a DAO proposal by its ID + pub async fn get_dao_proposal_by_id(&self, proposal_id: u64) -> Result { + // Grab the proposal record + let row = match self + .wallet + .query_single( + DAO_PROPOSALS_TABLE, + &[], + convert_named_params! {(DAO_PROPOSALS_COL_PROPOSAL_ID, proposal_id)}, + ) + .await + { + Ok(r) => r, + Err(e) => { + return Err(Error::RusqliteError(format!( + "[get_dao_proposal_by_id] DAO proposal retrieval failed: {e:?}" + ))) + } + }; + + // Parse DAO ID to grab the DAO record + let Value::Integer(dao_id) = row[1] else { + return Err(Error::ParseFailed("[get_dao_proposal_by_id] DAO ID parsing failed")) + }; + let Ok(dao_id) = u64::try_from(dao_id) else { + return Err(Error::ParseFailed("[get_dao_proposal_by_id] DAO ID parsing failed")) + }; + let dao = self.get_dao_by_id(dao_id).await?; + + // Parse rest of the record + self.parse_dao_proposal(&dao, &row) + } + + // Fetch all known DAO proposal votes from the wallet given a proposal ID + pub async fn get_dao_proposal_votes(&self, proposal_id: u64) -> Result> { + let rows = match self + .wallet + .query_multiple( + DAO_VOTES_TABLE, + &[], + convert_named_params! {(DAO_VOTES_COL_PROPOSAL_ID, proposal_id)}, + ) + .await + { + Ok(r) => r, + Err(e) => { + return Err(Error::RusqliteError(format!( + "[get_dao_proposal_votes] Votes retrieval failed: {e:?}" + ))) + } + }; + + let mut votes = Vec::with_capacity(rows.len()); + for row in rows { + let Value::Integer(id) = row[0] else { + return Err(Error::ParseFailed("[get_dao_proposal_votes] ID parsing failed")) + }; + let Ok(id) = u64::try_from(id) else { + return Err(Error::ParseFailed("[get_dao_proposal_votes] ID parsing failed")) + }; + + let Value::Integer(proposal_id) = row[1] else { + return Err(Error::ParseFailed( + "[get_dao_proposal_votes] Proposal ID parsing failed", + )) + }; + let Ok(proposal_id) = u64::try_from(proposal_id) else { + return Err(Error::ParseFailed( + "[get_dao_proposal_votes] Proposal ID parsing failed", + )) + }; + + let Value::Integer(vote_option) = row[2] else { + return Err(Error::ParseFailed( + "[get_dao_proposal_votes] Vote option parsing failed", + )) + }; + let Ok(vote_option) = u32::try_from(vote_option) else { + return Err(Error::ParseFailed( + "[get_dao_proposal_votes] Vote option parsing failed", + )) + }; + let vote_option = vote_option != 0; + + let Value::Blob(ref yes_vote_blind_bytes) = row[3] else { + return Err(Error::ParseFailed( + "[get_dao_proposal_votes] Yes vote blind bytes parsing failed", + )) + }; + let yes_vote_blind = deserialize(yes_vote_blind_bytes)?; + + let Value::Blob(ref all_vote_value_bytes) = row[4] else { + return Err(Error::ParseFailed( + "[get_dao_proposal_votes] All vote value bytes parsing failed", + )) + }; + let all_vote_value = deserialize(all_vote_value_bytes)?; + + let Value::Blob(ref all_vote_blind_bytes) = row[5] else { + return Err(Error::ParseFailed( + "[get_dao_proposal_votes] All vote blind bytes parsing failed", + )) + }; + let all_vote_blind = deserialize(all_vote_blind_bytes)?; + + let Value::Blob(ref tx_hash_bytes) = row[6] else { + return Err(Error::ParseFailed( + "[get_dao_proposal_votes] Transaction hash bytes parsing failed", + )) + }; + let tx_hash = + if tx_hash_bytes.is_empty() { None } else { Some(deserialize(tx_hash_bytes)?) }; + + let Value::Integer(call_index) = row[7] else { + return Err(Error::ParseFailed("[get_dao_proposal_votes] Call index parsing failed")) + }; + let Ok(call_index) = u32::try_from(call_index) else { + return Err(Error::ParseFailed("[get_dao_proposal_votes] Call index parsing failed")) + }; + let call_index = Some(call_index); + + let vote = DaoVote { + id, + proposal_id, + vote_option, + yes_vote_blind, + all_vote_value, + all_vote_blind, + tx_hash, + call_index, + }; + + votes.push(vote); + } + + Ok(votes) + } + + /// Mint a DAO on-chain + pub async fn dao_mint(&self, dao_id: u64) -> Result { + let dao = self.get_dao_by_id(dao_id).await?; + + if dao.tx_hash.is_some() { + return Err(Error::Custom( + "[dao_mint] This DAO seems to have already been minted on-chain".to_string(), + )) + } + + // TODO: Simplify this model struct import once + // we use the structs from contract everwhere + let dao_info = darkfi_dao_contract::model::Dao { + proposer_limit: dao.proposer_limit, + quorum: dao.quorum, + approval_ratio_base: dao.approval_ratio_base, + approval_ratio_quot: dao.approval_ratio_quot, + gov_token_id: dao.gov_token_id, + public_key: PublicKey::from_secret(dao.secret_key), + bulla_blind: dao.bulla_blind, + }; + + let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?; + let Some(dao_mint_zkbin) = zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_MINT_NS) + else { + return Err(Error::RusqliteError("[dao_mint] DAO Mint circuit not found".to_string())) + }; + + let dao_mint_zkbin = ZkBinary::decode(&dao_mint_zkbin.1)?; + let dao_mint_circuit = ZkCircuit::new(empty_witnesses(&dao_mint_zkbin)?, &dao_mint_zkbin); + eprintln!("Creating DAO Mint proving key"); + let dao_mint_pk = ProvingKey::build(dao_mint_zkbin.k, &dao_mint_circuit); + + let (params, proofs) = + make_mint_call(&dao_info, &dao.secret_key, &dao_mint_zkbin, &dao_mint_pk)?; + + let mut data = vec![DaoFunction::Mint as u8]; + params.encode(&mut data)?; + let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data }; + let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?; + let mut tx = tx_builder.build()?; + let sigs = tx.create_sigs(&mut OsRng, &[dao.secret_key])?; + tx.signatures = vec![sigs]; + + Ok(tx) + } + + /// Create a DAO proposal + pub async fn dao_propose( + &self, + dao_id: u64, + _recipient: PublicKey, + amount: u64, + token_id: TokenId, + ) -> Result { + let Ok(dao) = self.get_dao_by_id(dao_id).await else { + return Err(Error::RusqliteError("[dao_propose] DAO not found in wallet".to_string())) + }; + + if dao.leaf_position.is_none() || dao.tx_hash.is_none() { + return Err(Error::Custom( + "[dao_propose] DAO seems to not have been deployed yet".to_string(), + )) + } + + let bulla = dao.bulla(); + let owncoins = self.get_coins(false).await?; + + let mut dao_owncoins: Vec = owncoins.iter().map(|x| x.0.clone()).collect(); + dao_owncoins.retain(|x| { + x.note.token_id == token_id && + x.note.spend_hook == DAO_CONTRACT_ID.inner() && + x.note.user_data == bulla.inner() + }); + + let mut gov_owncoins: Vec = owncoins.iter().map(|x| x.0.clone()).collect(); + gov_owncoins.retain(|x| x.note.token_id == dao.gov_token_id); + + if dao_owncoins.is_empty() { + return Err(Error::Custom(format!( + "[dao_propose] Did not find any {token_id} coins owned by this DAO" + ))) + } + + if gov_owncoins.is_empty() { + return Err(Error::Custom(format!( + "[dao_propose] Did not find any governance {} coins in wallet", + dao.gov_token_id + ))) + } + + if dao_owncoins.iter().map(|x| x.note.value).sum::() < amount { + return Err(Error::Custom(format!( + "[dao_propose] Not enough DAO balance for token ID: {}", + token_id + ))) + } + + if gov_owncoins.iter().map(|x| x.note.value).sum::() < dao.proposer_limit { + return Err(Error::Custom(format!( + "[dao_propose] Not enough gov token {} balance to propose", + dao.gov_token_id + ))) + } + + // FIXME: Here we're looking for a coin == proposer_limit but this shouldn't have to + // be the case { + let Some(gov_coin) = gov_owncoins.iter().find(|x| x.note.value == dao.proposer_limit) + else { + return Err(Error::Custom(format!( + "[dao_propose] Did not find a single gov coin of value {}", + dao.proposer_limit + ))) + }; + // } + + // Lookup the zkas bins + let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?; + let Some(propose_burn_zkbin) = + zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_PROPOSE_INPUT_NS) + else { + return Err(Error::Custom("[dao_propose] Propose Burn circuit not found".to_string())) + }; + + let Some(propose_main_zkbin) = + zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_PROPOSE_MAIN_NS) + else { + return Err(Error::Custom("[dao_propose] Propose Main circuit not found".to_string())) + }; + + let propose_burn_zkbin = ZkBinary::decode(&propose_burn_zkbin.1)?; + let propose_main_zkbin = ZkBinary::decode(&propose_main_zkbin.1)?; + + let propose_burn_circuit = + ZkCircuit::new(empty_witnesses(&propose_burn_zkbin)?, &propose_burn_zkbin); + let propose_main_circuit = + ZkCircuit::new(empty_witnesses(&propose_main_zkbin)?, &propose_main_zkbin); + + eprintln!("Creating Propose Burn circuit proving key"); + let propose_burn_pk = ProvingKey::build(propose_burn_zkbin.k, &propose_burn_circuit); + eprintln!("Creating Propose Main circuit proving key"); + let propose_main_pk = ProvingKey::build(propose_main_zkbin.k, &propose_main_circuit); + + // Now create the parameters for the proposal tx + let signature_secret = SecretKey::random(&mut OsRng); + + // Get the Merkle path for the gov coin in the money tree + let money_merkle_tree = self.get_money_tree().await?; + let gov_coin_merkle_path = money_merkle_tree.witness(gov_coin.leaf_position, 0).unwrap(); + + // Fetch the daos Merkle tree + let (daos_tree, _) = self.get_dao_trees().await?; + + let input = DaoProposeStakeInput { + secret: gov_coin.secret, // <-- TODO: Is this correct? + note: gov_coin.note.clone(), + leaf_position: gov_coin.leaf_position, + merkle_path: gov_coin_merkle_path, + signature_secret, + }; + + let (dao_merkle_path, dao_merkle_root) = { + let root = daos_tree.root(0).unwrap(); + let leaf_pos = dao.leaf_position.unwrap(); + let dao_merkle_path = daos_tree.witness(leaf_pos, 0).unwrap(); + (dao_merkle_path, root) + }; + + // TODO: + /* + // Convert coin_params to actual coins + let mut proposal_coins = vec![]; + for coin_params in proposal_coinattrs { + proposal_coins.push(coin_params.to_coin()); + } + */ + let proposal_data = vec![]; + //proposal_coins.encode(&mut proposal_data).unwrap(); + + let auth_calls = vec![ + DaoAuthCall { + contract_id: DAO_CONTRACT_ID.inner(), + function_code: DaoFunction::AuthMoneyTransfer as u8, + auth_data: proposal_data, + }, + DaoAuthCall { + contract_id: MONEY_CONTRACT_ID.inner(), + function_code: MoneyFunction::TransferV1 as u8, + auth_data: vec![], + }, + ]; + + // TODO: get current height to calculate day + // Also contract must check we don't mint a proposal that its creation day is + // less than current height + + // TODO: Simplify this model struct import once + // we use the structs from contract everwhere + let proposal = darkfi_dao_contract::model::DaoProposal { + auth_calls, + creation_day: 0, + duration_days: 30, + user_data: pallas::Base::ZERO, + dao_bulla: dao.bulla(), + blind: pallas::Base::random(&mut OsRng), + }; + + // TODO: Simplify this model struct import once + // we use the structs from contract everwhere + let daoinfo = darkfi_dao_contract::model::Dao { + proposer_limit: dao.proposer_limit, + quorum: dao.quorum, + approval_ratio_quot: dao.approval_ratio_quot, + approval_ratio_base: dao.approval_ratio_base, + gov_token_id: dao.gov_token_id, + public_key: PublicKey::from_secret(dao.secret_key), + bulla_blind: dao.bulla_blind, + }; + + let call = DaoProposeCall { + inputs: vec![input], + proposal, + dao: daoinfo, + dao_leaf_position: dao.leaf_position.unwrap(), + dao_merkle_path, + dao_merkle_root, + }; + + eprintln!("Creating ZK proofs..."); + let (params, proofs) = call.make( + &propose_burn_zkbin, + &propose_burn_pk, + &propose_main_zkbin, + &propose_main_pk, + )?; + + let mut data = vec![DaoFunction::Propose as u8]; + params.encode(&mut data)?; + let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data }; + let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?; + let mut tx = tx_builder.build()?; + let sigs = tx.create_sigs(&mut OsRng, &[signature_secret])?; + tx.signatures = vec![sigs]; + + Ok(tx) + } + + /// Vote on a DAO proposal + pub async fn dao_vote( + &self, + dao_id: u64, + proposal_id: u64, + vote_option: bool, + weight: u64, + ) -> Result { + let dao = self.get_dao_by_id(dao_id).await?; + let proposals = self.get_dao_proposals(dao_id).await?; + let Some(proposal) = proposals.iter().find(|x| x.id == proposal_id) else { + return Err(Error::Custom("[dao_vote] Proposal ID not found".to_string())) + }; + + let money_tree = proposal.money_snapshot_tree.clone().unwrap(); + + let mut coins: Vec = + self.get_coins(false).await?.iter().map(|x| x.0.clone()).collect(); + + coins.retain(|x| x.note.token_id == dao.gov_token_id); + coins.retain(|x| x.note.spend_hook == pallas::Base::zero()); + + if coins.iter().map(|x| x.note.value).sum::() < weight { + return Err(Error::Custom("[dao_vote] Not enough balance for vote weight".to_string())) + } + + // TODO: The spent coins need to either be marked as spent here, and/or on scan + let mut spent_value = 0; + let mut spent_coins = vec![]; + let mut inputs = vec![]; + let mut input_secrets = vec![]; + + // FIXME: We don't take back any change so it's possible to vote with > requested weight. + for coin in coins { + if spent_value >= weight { + break + } + + spent_value += coin.note.value; + spent_coins.push(coin.clone()); + + let signature_secret = SecretKey::random(&mut OsRng); + input_secrets.push(signature_secret); + + let leaf_position = coin.leaf_position; + let merkle_path = money_tree.witness(coin.leaf_position, 0).unwrap(); + + let input = DaoVoteInput { + secret: coin.secret, + note: coin.note.clone(), + leaf_position, + merkle_path, + signature_secret, + }; + + inputs.push(input); + } + + // We use the DAO secret to encrypt the vote. + let dao_keypair = Keypair::new(dao.secret_key); + + // TODO: Fix this + // TODO: Simplify this model struct import once + // we use the structs from contract everwhere + let proposal = darkfi_dao_contract::model::DaoProposal { + auth_calls: vec![], + creation_day: 0, + duration_days: 30, + user_data: pallas::Base::ZERO, + dao_bulla: dao.bulla(), + blind: pallas::Base::random(&mut OsRng), + }; + + // TODO: Simplify this model struct import once + // we use the structs from contract everwhere + let dao_info = darkfi_dao_contract::model::Dao { + proposer_limit: dao.proposer_limit, + quorum: dao.quorum, + approval_ratio_quot: dao.approval_ratio_quot, + approval_ratio_base: dao.approval_ratio_base, + gov_token_id: dao.gov_token_id, + public_key: PublicKey::from_secret(dao.secret_key), + bulla_blind: dao.bulla_blind, + }; + + // TODO: get current height to calculate day + + let call = DaoVoteCall { + inputs, + vote_option, + current_day: 0, + dao_keypair, + proposal, + dao: dao_info, + }; + + let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?; + let Some(dao_vote_burn_zkbin) = + zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_VOTE_INPUT_NS) + else { + return Err(Error::Custom("[dao_vote] DAO Vote Burn circuit not found".to_string())) + }; + + let Some(dao_vote_main_zkbin) = + zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_VOTE_MAIN_NS) + else { + return Err(Error::Custom("[dao_vote] DAO Vote Main circuit not found".to_string())) + }; + + let dao_vote_burn_zkbin = ZkBinary::decode(&dao_vote_burn_zkbin.1)?; + let dao_vote_main_zkbin = ZkBinary::decode(&dao_vote_main_zkbin.1)?; + + let dao_vote_burn_circuit = + ZkCircuit::new(empty_witnesses(&dao_vote_burn_zkbin)?, &dao_vote_burn_zkbin); + let dao_vote_main_circuit = + ZkCircuit::new(empty_witnesses(&dao_vote_main_zkbin)?, &dao_vote_main_zkbin); + + eprintln!("Creating DAO Vote Burn proving key"); + let dao_vote_burn_pk = ProvingKey::build(dao_vote_burn_zkbin.k, &dao_vote_burn_circuit); + eprintln!("Creating DAO Vote Main proving key"); + let dao_vote_main_pk = ProvingKey::build(dao_vote_main_zkbin.k, &dao_vote_main_circuit); + + let (params, proofs) = call.make( + &dao_vote_burn_zkbin, + &dao_vote_burn_pk, + &dao_vote_main_zkbin, + &dao_vote_main_pk, + )?; + + let mut data = vec![DaoFunction::Vote as u8]; + params.encode(&mut data)?; + let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data }; + let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?; + let mut tx = tx_builder.build()?; + let sigs = tx.create_sigs(&mut OsRng, &input_secrets)?; + tx.signatures = vec![sigs]; + + Ok(tx) + } + + /// Import given DAO votes into the wallet + /// This function is really bad but I'm also really tired and annoyed. + pub async fn dao_exec(&self, _dao: Dao, _proposal: DaoProposal) -> Result { + // TODO + unimplemented!() + } } diff --git a/bin/drk2/src/main.rs b/bin/drk2/src/main.rs index fab0745b1..cae719b1b 100644 --- a/bin/drk2/src/main.rs +++ b/bin/drk2/src/main.rs @@ -35,7 +35,11 @@ use darkfi::{ async_daemonize, cli_desc, rpc::{client::RpcClient, jsonrpc::JsonRequest, util::JsonValue}, tx::Transaction, - util::{parse::encode_base10, path::expand_path}, + util::{ + parse::{decode_base10, encode_base10}, + path::expand_path, + }, + zk::halo2::Field, Result, }; use darkfi_money_contract::model::Coin; @@ -71,6 +75,7 @@ use money::BALANCE_BASE10_DECIMALS; /// Wallet functionality related to Dao mod dao; +use dao::DaoParams; /// Wallet functionality related to transactions history mod txs_history; @@ -82,6 +87,8 @@ use walletdb::{WalletDb, WalletPtr}; 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!())] @@ -115,6 +122,8 @@ struct Args { 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 @@ -209,7 +218,13 @@ enum Subcmd { /// find coins sent to us and fill our wallet with the necessary metadata. Subscribe, - // TODO: DAO + /// DAO functionalities + Dao { + #[structopt(subcommand)] + /// Sub command to execute + command: DaoSubcmd, + }, + /// Scan the blockchain and parse relevant transactions Scan { #[structopt(long)] @@ -270,6 +285,102 @@ enum OtcSubcmd { 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, + /// 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_alias: 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(Clone, Debug, Deserialize, StructOpt)] enum ExplorerSubcmd { /// Fetch a blockchain transaction by hash @@ -800,6 +911,280 @@ async fn realmain(args: Args, ex: Arc>) -> Result<()> { } } + Subcmd::Dao { command } => match command { + DaoSubcmd::Create { proposer_limit, 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); + } + + let proposer_limit = decode_base10(&proposer_limit, BALANCE_BASE10_DECIMALS, true)?; + let quorum = decode_base10(&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 = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).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 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(); + eprintln!("{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)?; + eprintln!("{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.wallet_path, args.wallet_pass, args.endpoint, ex).await?; + + if let Err(e) = drk.import_dao(dao_name, dao_params).await { + eprintln!("Failed to import DAO: {e:?}"); + exit(2); + } + + Ok(()) + } + + DaoSubcmd::List { dao_alias } => { + let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; + // We cannot use .map() since get_dao_id() uses ? + let dao_id = match dao_alias { + Some(alias) => Some(drk.get_dao_id(&alias).await?), + None => None, + }; + + if let Err(e) = drk.dao_list(dao_id).await { + eprintln!("Failed to list DAO: {e:?}"); + exit(2); + } + + Ok(()) + } + + DaoSubcmd::Balance { dao_alias } => { + let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; + let dao_id = drk.get_dao_id(&dao_alias).await?; + + let balmap = match drk.dao_balance(dao_id).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() { + eprintln!("No unspent balances found"); + } else { + eprintln!("{table}"); + } + + Ok(()) + } + + DaoSubcmd::Mint { dao_alias } => { + let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; + let dao_id = drk.get_dao_id(&dao_alias).await?; + + let tx = match drk.dao_mint(dao_id).await { + Ok(tx) => tx, + Err(e) => { + eprintln!("Failed to mint DAO: {e:?}"); + exit(2); + } + }; + eprintln!("{}", bs58::encode(&serialize(&tx)).into_string()); + Ok(()) + } + + DaoSubcmd::Propose { dao_alias, recipient, amount, token } => { + if let Err(e) = f64::from_str(&amount) { + eprintln!("Invalid amount: {e:?}"); + exit(2); + } + let amount = decode_base10(&amount, BALANCE_BASE10_DECIMALS, true)?; + let rcpt = match PublicKey::from_str(&recipient) { + Ok(r) => r, + Err(e) => { + eprintln!("Invalid recipient: {e:?}"); + exit(2); + } + }; + + let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; + let dao_id = drk.get_dao_id(&dao_alias).await?; + let token_id = match drk.get_token(token).await { + Ok(t) => t, + Err(e) => { + eprintln!("Invalid token alias: {e:?}"); + exit(2); + } + }; + + let tx = match drk.dao_propose(dao_id, rcpt, amount, token_id).await { + Ok(tx) => tx, + Err(e) => { + eprintln!("Failed to create DAO proposal: {e:?}"); + exit(2); + } + }; + eprintln!("{}", bs58::encode(&serialize(&tx)).into_string()); + Ok(()) + } + + DaoSubcmd::Proposals { dao_alias } => { + let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; + let dao_id = drk.get_dao_id(&dao_alias).await?; + + let proposals = drk.get_dao_proposals(dao_id).await?; + + for proposal in proposals { + eprintln!("[{}] {:?}", proposal.id, proposal.bulla()); + } + + Ok(()) + } + + DaoSubcmd::Proposal { dao_alias, proposal_id } => { + let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).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(2); + }; + + eprintln!("{proposal}"); + + let votes = drk.get_dao_proposal_votes(proposal_id).await?; + eprintln!("votes:"); + for vote in votes { + let option = if vote.vote_option { "yes" } else { "no " }; + eprintln!(" {option} {}", vote.all_vote_value); + } + + Ok(()) + } + + DaoSubcmd::Vote { dao_alias, proposal_id, vote, vote_weight } => { + let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; + let dao_id = drk.get_dao_id(&dao_alias).await?; + + if let Err(e) = f64::from_str(&vote_weight) { + eprintln!("Invalid vote weight: {e:?}"); + exit(2); + } + let weight = decode_base10(&vote_weight, BALANCE_BASE10_DECIMALS, true)?; + + if vote > 1 { + eprintln!("Vote can be either 0 (NO) or 1 (YES)"); + exit(2); + } + let vote = vote != 0; + + let tx = match drk.dao_vote(dao_id, proposal_id, vote, weight).await { + Ok(tx) => tx, + Err(e) => { + eprintln!("Failed to create DAO Vote transaction: {e:?}"); + exit(2); + } + }; + + // TODO: Write our_vote in the proposal sql. + + eprintln!("{}", bs58::encode(&serialize(&tx)).into_string()); + + Ok(()) + } + + DaoSubcmd::Exec { dao_alias, proposal_id } => { + let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).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 = match drk.dao_exec(dao, proposal).await { + Ok(tx) => tx, + Err(e) => { + eprintln!("Failed to execute DAO proposal: {e:?}"); + exit(2); + } + }; + eprintln!("{}", bs58::encode(&serialize(&tx)).into_string()); + + Ok(()) + } + }, + Subcmd::Inspect => { let mut buf = String::new(); stdin().read_to_string(&mut buf)?; @@ -863,15 +1248,13 @@ async fn realmain(args: Args, ex: Arc>) -> Result<()> { if list { eprintln!("List requested."); // TODO: implement - - return Ok(()) + unimplemented!() } if let Some(c) = checkpoint { eprintln!("Checkpoint requested: {c}"); // TODO: implement - - return Ok(()) + unimplemented!() } if let Err(e) = drk.scan_blocks(false).await {