Переглянути джерело

drk2: Dao functionality added

skoupidi 2 роки тому
батько
коміт
2f6bb5748f
3 змінених файлів з 1386 додано та 163 видалено
  1. 91 9
      bin/drk2/src/cli_util.rs
  2. 906 148
      bin/drk2/src/dao.rs
  3. 389 6
      bin/drk2/src/main.rs

+ 91 - 9
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,

+ 906 - 148
bin/drk2/src/dao.rs

@@ -16,36 +16,55 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-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<Vec<SecretKey>> {
         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<DaoProposal> {
+        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<Vec<DaoProposal>> {
         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::<DaoVoteNote>(&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");
+                    // 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
                         }
+                    }
 
-                        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<u64> {
+        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<u64> {
+        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<Dao> {
+        // 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<u64>) -> 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<HashMap<String, u64>> {
+        // 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<String, u64> = 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<DaoProposal> {
+        // 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<Vec<DaoVote>> {
+        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<Transaction> {
+        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<Transaction> {
+        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<OwnCoin> = 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<OwnCoin> = 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::<u64>() < 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::<u64>() < 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<Transaction> {
+        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<OwnCoin> =
+            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::<u64>() < 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<Transaction> {
+        // TODO
+        unimplemented!()
+    }
 }

+ 389 - 6
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<String>,
+    },
+
+    /// 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<smol::Executor<'static>>) -> 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<smol::Executor<'static>>) -> 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 {