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 {