parazyd 3 лет назад
Родитель
Сommit
75bcaa1f0d
4 измененных файлов с 682 добавлено и 165 удалено
  1. 1 1
      bin/drk/Cargo.toml
  2. 63 122
      bin/drk/src/main.rs
  3. 219 5
      bin/drk/src/rpc_dao.rs
  4. 399 37
      bin/drk/src/wallet_dao.rs

+ 1 - 1
bin/drk/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 name = "drk"
-version = "0.3.0"
+version = "0.4.0"
 homepage = "https://dark.fi"
 description = "Command-line client for darkfid"
 authors = ["Dyne.org foundation <foundation@dyne.org>"]

+ 63 - 122
bin/drk/src/main.rs

@@ -236,9 +236,9 @@ enum DaoSubcmd {
     /// Create DAO parameters
     Create {
         /// The minimum amount of governance tokens needed to open a proposal for this DAO
-        proposer_limit: u64,
+        proposer_limit: String,
         /// Minimal threshold of participating total tokens needed for a proposal to pass
-        quorum: u64,
+        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
@@ -308,7 +308,7 @@ enum DaoSubcmd {
         dao_id: u64,
 
         /// Numeric identifier for the proposal
-        proposal: u64,
+        proposal_id: u64,
 
         /// Vote (0 for NO, 1 for YES)
         vote: u8,
@@ -322,8 +322,8 @@ enum DaoSubcmd {
         /// Numeric identifier for the DAO
         dao_id: u64,
 
-        /// Proposal identifier
-        proposal: String,
+        /// Numeric identifier for the proposal
+        proposal_id: u64,
     },
 }
 
@@ -332,6 +332,11 @@ pub struct Drk {
 }
 
 impl Drk {
+    async fn new(endpoint: Url) -> Result<Self> {
+        let rpc_client = RpcClient::new(endpoint).await?;
+        Ok(Self { rpc_client })
+    }
+
     async fn ping(&self) -> Result<()> {
         let latency = Instant::now();
         let req = JsonRequest::new("ping", json!([]));
@@ -355,12 +360,9 @@ async fn main() -> Result<()> {
 
     match args.command {
         Subcmd::Ping => {
-            let rpc_client = RpcClient::new(args.endpoint)
-                .await
-                .with_context(|| "Could not connect to darkfid RPC endpoint")?;
-
-            let drk = Drk { rpc_client };
+            let drk = Drk::new(args.endpoint).await?;
             drk.ping().await.with_context(|| "Failed to ping darkfid RPC endpoint")?;
+
             Ok(())
         }
 
@@ -388,11 +390,7 @@ async fn main() -> Result<()> {
                 exit(2);
             }
 
-            let rpc_client = RpcClient::new(args.endpoint)
-                .await
-                .with_context(|| "Could not connect to darkfid RPC endpoint")?;
-
-            let drk = Drk { rpc_client };
+            let drk = Drk::new(args.endpoint).await?;
 
             if initialize {
                 drk.initialize_money().await?;
@@ -429,7 +427,7 @@ async fn main() -> Result<()> {
 
             if address {
                 let address = drk
-                    .wallet_address(1)
+                    .wallet_address(1) // <-- TODO: Use is_default from the sql table
                     .await
                     .with_context(|| "Failed to fetch default address")?;
 
@@ -508,7 +506,7 @@ async fn main() -> Result<()> {
                 table.set_titles(row!["Coin", "Spent", "Token ID", "Value"]);
                 for coin in coins {
                     table.add_row(row![
-                        format!("{:?}", coin.0.coin.inner()),
+                        format!("{}", bs58::encode(&serialize(&coin.0.coin.inner())).into_string()),
                         coin.1,
                         coin.0.note.token_id,
                         format!("{} ({})", coin.0.note.value, encode_base10(coin.0.note.value, 8))
@@ -532,12 +530,7 @@ async fn main() -> Result<()> {
             };
 
             let coin = Coin::from(elem);
-
-            let rpc_client = RpcClient::new(args.endpoint)
-                .await
-                .with_context(|| "Could not connect to darkfid RPC endpoint")?;
-
-            let drk = Drk { rpc_client };
+            let drk = Drk::new(args.endpoint).await?;
             drk.unspend_coin(&coin).await.with_context(|| "Failed to mark coin as unspent")?;
 
             Ok(())
@@ -547,11 +540,7 @@ async fn main() -> Result<()> {
             let amount = f64::from_str(&amount).with_context(|| "Invalid amount")?;
             let token_id = TokenId::try_from(token.as_str()).with_context(|| "Invalid Token ID")?;
 
-            let rpc_client = RpcClient::new(args.endpoint)
-                .await
-                .with_context(|| "Could not connect to darkfid RPC endpoint")?;
-
-            let drk = Drk { rpc_client };
+            let drk = Drk::new(args.endpoint).await?;
 
             let address = match address {
                 Some(v) => PublicKey::from_str(v.as_str()).with_context(|| "Invalid address")?,
@@ -566,6 +555,7 @@ async fn main() -> Result<()> {
                 .with_context(|| "Failed to request airdrop")?;
 
             println!("Transaction ID: {}", txid);
+
             Ok(())
         }
 
@@ -574,11 +564,7 @@ async fn main() -> Result<()> {
             let token_id = TokenId::try_from(token.as_str()).with_context(|| "Invalid Token ID")?;
             let rcpt = PublicKey::from_str(&recipient).with_context(|| "Invalid recipient")?;
 
-            let rpc_client = RpcClient::new(args.endpoint)
-                .await
-                .with_context(|| "Could not connect to darkfid RPC endpoint")?;
-
-            let drk = Drk { rpc_client };
+            let drk = Drk::new(args.endpoint).await?;
 
             let tx = drk
                 .transfer(&amount, token_id, rcpt, dao, dao_bulla)
@@ -591,11 +577,7 @@ async fn main() -> Result<()> {
         }
 
         Subcmd::Otc(cmd) => {
-            let rpc_client = RpcClient::new(args.endpoint)
-                .await
-                .with_context(|| "Could not connect to darkfid RPC endpoint")?;
-
-            let drk = Drk { rpc_client };
+            let drk = Drk::new(args.endpoint).await?;
 
             match cmd {
                 OtcSubcmd::Init { value_pair, token_pair } => {
@@ -667,26 +649,18 @@ async fn main() -> Result<()> {
             let bytes = bs58::decode(&buf.trim()).into_vec()?;
             let tx = deserialize(&bytes)?;
 
-            let rpc_client = RpcClient::new(args.endpoint)
-                .await
-                .with_context(|| "Could not connect to darkfid RPC endpoint")?;
-
-            let drk = Drk { rpc_client };
+            let drk = Drk::new(args.endpoint).await?;
 
             let txid =
                 drk.broadcast_tx(&tx).await.with_context(|| "Failed to broadcast transaction")?;
 
-            eprintln!("Transaction ID: {}", txid);
+            println!("Transaction ID: {}", txid);
 
             Ok(())
         }
 
         Subcmd::Subscribe => {
-            let rpc_client = RpcClient::new(args.endpoint.clone())
-                .await
-                .with_context(|| "Could not connect to darkfid RPC endpoint")?;
-
-            let drk = Drk { rpc_client };
+            let drk = Drk::new(args.endpoint.clone()).await?;
 
             drk.subscribe_blocks(args.endpoint)
                 .await
@@ -696,11 +670,7 @@ async fn main() -> Result<()> {
         }
 
         Subcmd::Scan { reset, list, checkpoint } => {
-            let rpc_client = RpcClient::new(args.endpoint)
-                .await
-                .with_context(|| "Could not connect to darkfid RPC endpoint")?;
-
-            let drk = Drk { rpc_client };
+            let drk = Drk::new(args.endpoint).await?;
 
             if reset {
                 eprintln!("Reset requested.");
@@ -731,6 +701,12 @@ async fn main() -> Result<()> {
 
         Subcmd::Dao(cmd) => match cmd {
             DaoSubcmd::Create { proposer_limit, quorum, approval_ratio, gov_token_id } => {
+                let _ = f64::from_str(&proposer_limit).with_context(|| "Invalid proposer limit")?;
+                let _ = f64::from_str(&quorum).with_context(|| "Invalid quorum")?;
+
+                let proposer_limit = decode_base10(&proposer_limit, 8, true)?;
+                let quorum = decode_base10(&quorum, 8, true)?;
+
                 if approval_ratio > 1.0 {
                     eprintln!("Error: Approval ratio cannot be >1.0");
                     exit(1);
@@ -757,6 +733,7 @@ async fn main() -> Result<()> {
 
                 let encoded = bs58::encode(&serialize(&dao_params)).into_string();
                 println!("{}", encoded);
+
                 Ok(())
             }
 
@@ -765,16 +742,8 @@ async fn main() -> Result<()> {
                 stdin().read_to_string(&mut buf)?;
                 let bytes = bs58::decode(&buf.trim()).into_vec()?;
                 let dao_params: DaoParams = deserialize(&bytes)?;
-                println!("DAO Parameters:");
-                println!("Proposer limit: {}", dao_params.proposer_limit);
-                println!("Quorum: {}", dao_params.quorum);
-                println!(
-                    "Approval ratio: {}",
-                    dao_params.approval_ratio_base as f64 / dao_params.approval_ratio_quot as f64
-                );
-                println!("Governance token ID: {}", dao_params.gov_token_id);
-                println!("Secret key: {}", dao_params.secret_key);
-                println!("Bulla blind: {:?}", dao_params.bulla_blind);
+                println!("{}", dao_params);
+
                 Ok(())
             }
 
@@ -784,11 +753,7 @@ async fn main() -> Result<()> {
                 let bytes = bs58::decode(&buf.trim()).into_vec()?;
                 let dao_params: DaoParams = deserialize(&bytes)?;
 
-                let rpc_client = RpcClient::new(args.endpoint.clone())
-                    .await
-                    .with_context(|| "Could not connect to darkfid RPC endpoint")?;
-
-                let drk = Drk { rpc_client };
+                let drk = Drk::new(args.endpoint).await?;
 
                 drk.import_dao(dao_name, dao_params)
                     .await
@@ -798,11 +763,7 @@ async fn main() -> Result<()> {
             }
 
             DaoSubcmd::List { dao_id } => {
-                let rpc_client = RpcClient::new(args.endpoint.clone())
-                    .await
-                    .with_context(|| "Could not connect to darkfid RPC endpoint")?;
-
-                let drk = Drk { rpc_client };
+                let drk = Drk::new(args.endpoint).await?;
 
                 drk.dao_list(dao_id).await.with_context(|| "Failed to list DAO")?;
 
@@ -810,11 +771,7 @@ async fn main() -> Result<()> {
             }
 
             DaoSubcmd::Balance { dao_id } => {
-                let rpc_client = RpcClient::new(args.endpoint.clone())
-                    .await
-                    .with_context(|| "Could not connect to darkfid RPC endpoint")?;
-
-                let drk = Drk { rpc_client };
+                let drk = Drk::new(args.endpoint).await?;
 
                 let balmap =
                     drk.dao_balance(dao_id).await.with_context(|| "Failed to fetch DAO balance")?;
@@ -838,11 +795,7 @@ async fn main() -> Result<()> {
             }
 
             DaoSubcmd::Mint { dao_id } => {
-                let rpc_client = RpcClient::new(args.endpoint.clone())
-                    .await
-                    .with_context(|| "Could not connect to darkfid RPC endpoint")?;
-
-                let drk = Drk { rpc_client };
+                let drk = Drk::new(args.endpoint).await?;
 
                 let tx = drk.dao_mint(dao_id).await.with_context(|| "Failed to mint DAO")?;
                 println!("{}", bs58::encode(&serialize(&tx)).into_string());
@@ -857,11 +810,7 @@ async fn main() -> Result<()> {
                 let token_id =
                     TokenId::try_from(token_id.as_str()).with_context(|| "Invalid Token ID")?;
 
-                let rpc_client = RpcClient::new(args.endpoint.clone())
-                    .await
-                    .with_context(|| "Could not connect to darkfid RPC endpoint")?;
-
-                let drk = Drk { rpc_client };
+                let drk = Drk::new(args.endpoint).await?;
 
                 let tx = drk
                     .dao_propose(dao_id, rcpt, amount, token_id)
@@ -873,13 +822,10 @@ async fn main() -> Result<()> {
             }
 
             DaoSubcmd::Proposals { dao_id } => {
-                let rpc_client = RpcClient::new(args.endpoint.clone())
-                    .await
-                    .with_context(|| "Could not connect to darkfid RPC endpoint")?;
-
-                let drk = Drk { rpc_client };
+                let drk = Drk::new(args.endpoint).await?;
 
                 let proposals = drk.get_dao_proposals(dao_id).await?;
+
                 for proposal in proposals {
                     println!("[{}] {:?}", proposal.id, proposal.bulla());
                 }
@@ -888,11 +834,7 @@ async fn main() -> Result<()> {
             }
 
             DaoSubcmd::Proposal { dao_id, proposal_id } => {
-                let rpc_client = RpcClient::new(args.endpoint.clone())
-                    .await
-                    .with_context(|| "Could not connect to darkfid RPC endpoint")?;
-
-                let drk = Drk { rpc_client };
+                let drk = Drk::new(args.endpoint).await?;
 
                 let proposals = drk.get_dao_proposals(dao_id).await?;
                 let Some(proposal) = proposals.iter().find(|x| x.id == proposal_id) else {
@@ -900,31 +842,13 @@ async fn main() -> Result<()> {
                     exit(1);
                 };
 
-                println!("Proposal parameters:");
-                println!("DAO Bulla: {}", proposal.dao_bulla);
-                println!("Recipient: {}", proposal.recipient);
-                println!(
-                    "Proposal amount {} ({})",
-                    encode_base10(proposal.amount, 8),
-                    proposal.amount
-                );
-                println!("Proposal serial: {:?}", proposal.serial);
-                println!("Proposal token ID: {}", proposal.token_id);
-                println!("Proposal bulla blind: {:?}", proposal.bulla_blind);
-                println!("Proposal leaf position: {:?}", proposal.leaf_position);
-                println!("Proposal tx hash: {:?}", proposal.tx_hash);
-                println!("Proposal call index: {:?}", proposal.call_index);
-                println!("Proposal vote ID: {:?}", proposal.vote_id);
+                println!("{}", proposal);
 
                 Ok(())
             }
 
-            DaoSubcmd::Vote { dao_id, proposal, vote, vote_weight } => {
-                let rpc_client = RpcClient::new(args.endpoint.clone())
-                    .await
-                    .with_context(|| "Could not connect to darkfid RPC endpoint")?;
-
-                let drk = Drk { rpc_client };
+            DaoSubcmd::Vote { dao_id, proposal_id, vote, vote_weight } => {
+                let drk = Drk::new(args.endpoint).await?;
 
                 let _ = f64::from_str(&vote_weight).with_context(|| "Invalid vote weight")?;
                 let weight = decode_base10(&vote_weight, 8, true)?;
@@ -936,15 +860,32 @@ async fn main() -> Result<()> {
                 let vote = vote != 0;
 
                 let tx = drk
-                    .dao_vote(dao_id, proposal, vote, weight)
+                    .dao_vote(dao_id, proposal_id, vote, weight)
                     .await
                     .with_context(|| "Failed to create DAO Vote transaction")?;
 
+                // TODO: Write our_vote in the proposal sql.
+
                 println!("{}", bs58::encode(&serialize(&tx)).into_string());
+
                 Ok(())
             }
 
-            DaoSubcmd::Exec { dao_id, proposal } => todo!(),
+            DaoSubcmd::Exec { dao_id, proposal_id } => {
+                let drk = Drk::new(args.endpoint).await?;
+                let dao = drk.get_dao_by_id(dao_id).await?;
+                let proposal = drk.get_dao_proposal_by_id(proposal_id).await?;
+                assert!(proposal.id == dao.id);
+
+                let tx = drk
+                    .dao_exec(dao, proposal)
+                    .await
+                    .with_context(|| "Failed to execute DAO proposal")?;
+
+                println!("{}", bs58::encode(&serialize(&tx)).into_string());
+
+                Ok(())
+            }
         },
     }
 }

+ 219 - 5
bin/drk/src/rpc_dao.rs

@@ -25,13 +25,19 @@ use darkfi::{
 use darkfi_dao_contract::{
     dao_client,
     dao_client::{DaoInfo, DaoProposalInfo, DaoVoteCall, DaoVoteInput},
-    DaoFunction, DAO_CONTRACT_ZKAS_DAO_MINT_NS, DAO_CONTRACT_ZKAS_DAO_PROPOSE_BURN_NS,
-    DAO_CONTRACT_ZKAS_DAO_PROPOSE_MAIN_NS, DAO_CONTRACT_ZKAS_DAO_VOTE_BURN_NS,
-    DAO_CONTRACT_ZKAS_DAO_VOTE_MAIN_NS,
+    dao_model::DaoBlindAggregateVote,
+    money_client, DaoFunction, DAO_CONTRACT_ZKAS_DAO_EXEC_NS, DAO_CONTRACT_ZKAS_DAO_MINT_NS,
+    DAO_CONTRACT_ZKAS_DAO_PROPOSE_BURN_NS, DAO_CONTRACT_ZKAS_DAO_PROPOSE_MAIN_NS,
+    DAO_CONTRACT_ZKAS_DAO_VOTE_BURN_NS, DAO_CONTRACT_ZKAS_DAO_VOTE_MAIN_NS,
+};
+use darkfi_money_contract::{
+    client::OwnCoin, MoneyFunction, MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
 };
-use darkfi_money_contract::client::OwnCoin;
 use darkfi_sdk::{
-    crypto::{Keypair, PublicKey, SecretKey, TokenId, DAO_CONTRACT_ID},
+    crypto::{
+        pedersen_commitment_u64, Keypair, PublicKey, SecretKey, TokenId, DAO_CONTRACT_ID,
+        MONEY_CONTRACT_ID,
+    },
     incrementalmerkletree::Tree,
     pasta::pallas,
     ContractCall,
@@ -40,6 +46,7 @@ use darkfi_serial::Encodable;
 use rand::rngs::OsRng;
 
 use super::Drk;
+use crate::wallet_dao::{Dao, DaoProposal};
 
 impl Drk {
     /// Mint a DAO on-chain
@@ -260,6 +267,7 @@ impl Drk {
             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(anyhow!("Not enough balance for vote weight"))
@@ -372,4 +380,210 @@ impl Drk {
 
         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> {
+        let dao_bulla = dao.bulla();
+        let votes = self.get_dao_proposal_votes(proposal.id).await?;
+
+        // Find the treasury coins that can be used for this proposal
+        let mut coins: Vec<OwnCoin> =
+            self.get_coins(false).await?.iter().map(|x| x.0.clone()).collect();
+        coins.retain(|x| x.note.spend_hook == DAO_CONTRACT_ID.inner());
+        coins.retain(|x| x.note.user_data == dao_bulla.inner());
+        coins.retain(|x| x.note.token_id == proposal.token_id);
+
+        if coins.iter().map(|x| x.note.value).sum::<u64>() < proposal.amount {
+            return Err(anyhow!("Not enough balance in DAO treasury to execute proposal"))
+        }
+
+        // Used to export user_data from this coin so it can be accessed by DAO::exec()
+        let user_data_blind = pallas::Base::random(&mut OsRng);
+
+        let user_serial = pallas::Base::random(&mut OsRng);
+        let user_coin_blind = pallas::Base::random(&mut OsRng);
+        let dao_serial = pallas::Base::random(&mut OsRng);
+        let dao_coin_blind = pallas::Base::random(&mut OsRng);
+        let input_value_blind = pallas::Scalar::random(&mut OsRng);
+
+        // TODO: FIXME: Clean this up and create an API
+        let exec_signature_secret = SecretKey::random(&mut OsRng);
+        let mut xfer_signature_secrets = vec![];
+        let mut xfer_inputs = vec![];
+
+        let mut input_coins = vec![];
+        let mut input_value_blinds = vec![];
+        let mut input_amount = 0;
+        for coin in coins {
+            input_amount += coin.note.value;
+            input_coins.push(coin);
+            if input_amount >= proposal.amount {
+                break
+            }
+        }
+
+        let money_merkle_tree = self.get_money_tree().await?;
+        let money_merkle_root = money_merkle_tree.root(0).unwrap();
+
+        for coin in &input_coins {
+            let value_blind = pallas::Scalar::random(&mut OsRng);
+            let sig_secret = SecretKey::random(&mut OsRng);
+            xfer_signature_secrets.push(sig_secret);
+
+            xfer_inputs.push(money_client::TransferInput {
+                leaf_position: coin.leaf_position,
+                merkle_path: money_merkle_tree
+                    .authentication_path(coin.leaf_position, &money_merkle_root)
+                    .unwrap(),
+                secret: dao.secret_key,
+                note: coin.note.clone(),
+                user_data_blind,
+                value_blind,
+                signature_secret: sig_secret,
+            });
+
+            input_value_blinds.push(value_blind);
+        }
+
+        let input_sum = input_coins.iter().map(|x| x.note.value).sum::<u64>();
+
+        // I'm tired
+        let xfer_outputs = vec![
+            // Proposal send
+            money_client::TransferOutput {
+                value: proposal.amount,
+                token_id: proposal.token_id,
+                public: proposal.recipient,
+                serial: user_serial,
+                coin_blind: user_coin_blind,
+                spend_hook: pallas::Base::zero(),
+                user_data: pallas::Base::zero(),
+            },
+            // Change
+            money_client::TransferOutput {
+                value: input_sum - proposal.amount,
+                token_id: proposal.token_id,
+                public: PublicKey::from_secret(dao.secret_key),
+                serial: dao_serial,
+                coin_blind: dao_coin_blind,
+                spend_hook: DAO_CONTRACT_ID.inner(),
+                user_data: dao_bulla.inner(),
+            },
+        ];
+
+        let xfer_call = money_client::TransferCall {
+            clear_inputs: vec![],
+            inputs: xfer_inputs,
+            outputs: xfer_outputs,
+        };
+
+        let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
+        let Some(mint_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_MINT_NS_V1) else {
+            return Err(anyhow!("Money Mint circuit not found"))
+        };
+        let Some(burn_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_BURN_NS_V1) else {
+            return Err(anyhow!("Money Burn circuit not found"))
+        };
+        let mint_zkbin = ZkBinary::decode(&mint_zkbin.1)?;
+        let burn_zkbin = ZkBinary::decode(&burn_zkbin.1)?;
+        let k = 13;
+        let mint_circuit = ZkCircuit::new(empty_witnesses(&mint_zkbin), mint_zkbin.clone());
+        let burn_circuit = ZkCircuit::new(empty_witnesses(&burn_zkbin), burn_zkbin.clone());
+        eprintln!("Creating Money Mint circuit proving key");
+        let mint_pk = ProvingKey::build(k, &mint_circuit);
+        eprintln!("Creating Money Burn circuit proving key");
+        let burn_pk = ProvingKey::build(k, &burn_circuit);
+
+        let (xfer_params, xfer_proofs) =
+            xfer_call.make(&mint_zkbin, &mint_pk, &burn_zkbin, &burn_pk)?;
+
+        let mut data = vec![MoneyFunction::Transfer as u8];
+        xfer_params.encode(&mut data)?;
+        let xfer_call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
+
+        let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?;
+        let Some(exec_zkbin) = zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_EXEC_NS) else {
+            return Err(anyhow!("DAO Exec circuit not found"))
+        };
+        let exec_zkbin = ZkBinary::decode(&exec_zkbin.1)?;
+        let exec_circuit = ZkCircuit::new(empty_witnesses(&exec_zkbin), exec_zkbin.clone());
+        eprintln!("Creating DAO Exec circuit proving key");
+        let exec_pk = ProvingKey::build(k, &exec_circuit);
+
+        // Count votes
+        let mut total_yes_vote_value = 0;
+        let mut total_all_vote_value = 0;
+        let mut blind_total_vote = DaoBlindAggregateVote::default();
+        let mut total_yes_vote_blind = pallas::Scalar::zero();
+        let mut total_all_vote_blind = pallas::Scalar::zero();
+
+        for (_, vote) in votes.iter().enumerate() {
+            total_yes_vote_blind += vote.yes_vote_blind;
+            total_all_vote_blind += vote.all_vote_blind;
+
+            let yes_vote_value = vote.vote_option as u64 * vote.all_vote_value;
+            total_yes_vote_value += yes_vote_value;
+            total_all_vote_value += vote.all_vote_value;
+
+            let yes_vote_commit = pedersen_commitment_u64(yes_vote_value, vote.yes_vote_blind);
+            let all_vote_commit = pedersen_commitment_u64(vote.all_vote_value, vote.all_vote_blind);
+
+            let blind_vote = DaoBlindAggregateVote { yes_vote_commit, all_vote_commit };
+            blind_total_vote.aggregate(blind_vote);
+        }
+
+        let prop_t = DaoProposalInfo {
+            dest: proposal.recipient,
+            amount: proposal.amount,
+            serial: proposal.serial,
+            token_id: proposal.token_id,
+            blind: proposal.bulla_blind, // <-- FIXME: wtf
+        };
+
+        let dao_t = DaoInfo {
+            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 dao_exec_call = dao_client::DaoExecCall {
+            proposal: prop_t,
+            dao: dao_t,
+            yes_vote_value: total_yes_vote_value,
+            all_vote_value: total_all_vote_value,
+            yes_vote_blind: total_yes_vote_blind,
+            all_vote_blind: total_all_vote_blind,
+            user_serial,
+            user_coin_blind,
+            dao_serial,
+            dao_coin_blind,
+            input_value: input_sum, // <-- FIXME
+            input_value_blind,      // <-- FIXME
+            hook_dao_exec: DAO_CONTRACT_ID.inner(),
+            signature_secret: exec_signature_secret,
+        };
+
+        let (exec_params, exec_proofs) = dao_exec_call.make(&exec_zkbin, &exec_pk)?;
+
+        let mut data = vec![DaoFunction::Exec as u8];
+        exec_params.encode(&mut data)?;
+        let exec_call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
+
+        let mut tx = Transaction {
+            calls: vec![xfer_call, exec_call],
+            proofs: vec![xfer_proofs, exec_proofs],
+            signatures: vec![],
+        };
+
+        let xfer_sigs = tx.create_sigs(&mut OsRng, &xfer_signature_secrets)?;
+        let exec_sigs = tx.create_sigs(&mut OsRng, &[exec_signature_secret])?;
+        tx.signatures = vec![xfer_sigs, exec_sigs];
+
+        Ok(tx)
+    }
 }

+ 399 - 37
bin/drk/src/wallet_dao.rs

@@ -16,25 +16,31 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use std::collections::HashMap;
+use std::{collections::HashMap, fmt};
 
 use anyhow::{anyhow, Result};
-use darkfi::{rpc::jsonrpc::JsonRequest, tx::Transaction, wallet::walletdb::QueryType};
+use darkfi::{
+    rpc::jsonrpc::JsonRequest, tx::Transaction, util::parse::encode_base10,
+    wallet::walletdb::QueryType,
+};
 use darkfi_dao_contract::{
     dao_client::{
-        DaoProposeNote, 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,
+        DaoProposeNote, DaoVoteNote, 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_OUR_VOTE_ID,
         DAO_PROPOSALS_COL_PROPOSAL_ID, DAO_PROPOSALS_COL_RECV_PUBLIC,
         DAO_PROPOSALS_COL_SENDCOIN_TOKEN_ID, DAO_PROPOSALS_COL_SERIAL, DAO_PROPOSALS_COL_TX_HASH,
         DAO_PROPOSALS_TABLE, DAO_TREES_COL_DAOS_TREE, DAO_TREES_COL_PROPOSALS_TREE,
-        DAO_TREES_TABLE,
+        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_ID, DAO_VOTES_COL_VOTE_OPTION, DAO_VOTES_COL_YES_VOTE_BLIND,
+        DAO_VOTES_TABLE,
     },
-    dao_model::{DaoBulla, DaoMintParams, DaoProposeParams},
+    dao_model::{DaoBulla, DaoMintParams, DaoProposeParams, DaoVoteParams},
     note::EncryptedNote2,
     DaoFunction,
 };
@@ -68,6 +74,34 @@ pub struct DaoParams {
     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_base as f64 / self.approval_ratio_quot 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 {
@@ -113,6 +147,44 @@ impl Dao {
     }
 }
 
+impl fmt::Display for Dao {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        let s = format!(
+            "{}\n{}\n{}: {}\n{}: {}\n{}: {} ({})\n{}: {} ({})\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {:?}\n{}: {:?}\n{}: {:?}\n{}: {:?}",
+            "DAO Parameters",
+            "==============",
+            "Name",
+            self.name,
+            "Bulla",
+            self.bulla(),
+            "Proposer limit",
+            encode_base10(self.proposer_limit, 8),
+            self.proposer_limit,
+            "Quorum",
+            encode_base10(self.quorum, 8),
+            self.quorum,
+            "Approval ratio",
+            self.approval_ratio_base as f64 / self.approval_ratio_quot 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,
+            "Leaf position",
+            self.leaf_position,
+            "Tx hash",
+            self.tx_hash,
+            "Call idx",
+            self.call_index,
+        );
+
+        write!(f, "{}", s)
+    }
+}
+
 #[derive(Debug, Clone)]
 /// Parameters representing an initialized DAO proposal, optionally deployed on-chain
 pub struct DaoProposal {
@@ -157,6 +229,39 @@ impl DaoProposal {
     }
 }
 
+impl fmt::Display for DaoProposal {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        let s = format!(
+            "{}\n{}\n{}: {}\n{}: {}\n{}: {} ({})\n{}: {:?}\n{}: {}\n{}: {:?}\n{}: {:?}\n{}: {:?}\n{}: {:?}\n{}: {:?}",
+            "Proposal parameters",
+            "===================",
+            "DAO Bulla",
+            self.dao_bulla,
+            "Recipient",
+            self.recipient,
+            "Proposal amount",
+            encode_base10(self.amount, 8),
+            self.amount,
+            "Proposal serial",
+            self.serial,
+            "Proposal Token ID",
+            self.token_id,
+            "Proposal bulla blind",
+            self.bulla_blind,
+            "Proposal leaf position",
+            self.leaf_position,
+            "Proposal tx hash",
+            self.tx_hash,
+            "Proposal call index",
+            self.call_index,
+            "Proposal vote ID",
+            self.vote_id,
+        );
+
+        write!(f, "{}", s)
+    }
+}
+
 #[derive(Debug, Clone)]
 /// Parameters representing a vote we've made on a DAO proposal
 pub struct DaoVote {
@@ -166,6 +271,12 @@ pub struct DaoVote {
     pub proposal_id: u64,
     /// The vote
     pub vote_option: bool,
+    /// Blinding factor for the yes vote
+    pub yes_vote_blind: pallas::Scalar,
+    /// Value of all votes
+    pub all_vote_value: u64,
+    /// Blinding facfor of all votes
+    pub all_vote_blind: pallas::Scalar,
     /// Transaction hash where this vote was casted
     pub tx_hash: Option<blake3::Hash>,
     /// call index in the transaction where this vote was casted
@@ -321,10 +432,10 @@ impl Drk {
             query,
             QueryType::Blob as u8,
             serialize(&dao_name),
-            QueryType::Integer as u8,
-            dao_params.proposer_limit,
-            QueryType::Integer as u8,
-            dao_params.quorum,
+            QueryType::Blob as u8,
+            serialize(&dao_params.proposer_limit),
+            QueryType::Blob as u8,
+            serialize(&dao_params.quorum),
             QueryType::Integer as u8,
             dao_params.approval_ratio_base,
             QueryType::Integer as u8,
@@ -362,22 +473,7 @@ impl Drk {
     async fn dao_list_single(&self, dao_id: u64) -> Result<()> {
         let dao = self.get_dao_by_id(dao_id).await?;
 
-        println!("DAO Parameters:");
-        println!("Name: {}", dao.name);
-        println!("Bulla: {}", dao.bulla());
-        println!("Proposer limit: {}", dao.proposer_limit);
-        println!("Quorum: {}", dao.quorum);
-        println!(
-            "Approval ratio: {}",
-            dao.approval_ratio_base as f64 / dao.approval_ratio_quot as f64
-        );
-        println!("Governance token ID: {}", dao.gov_token_id);
-        println!("Public key: {}", PublicKey::from_secret(dao.secret_key));
-        println!("Secret key: {}", dao.secret_key);
-        println!("Bulla blind: {:?}", dao.bulla_blind);
-        println!("Leaf position: {:?}", dao.leaf_position);
-        println!("Tx hash: {:?}", dao.tx_hash);
-        println!("Call idx: {:?}", dao.call_index);
+        println!("{}", dao);
 
         Ok(())
     }
@@ -403,9 +499,9 @@ impl Drk {
             DAO_DAOS_COL_DAO_ID,
             QueryType::Blob as u8,
             DAO_DAOS_COL_NAME,
-            QueryType::Integer as u8,
+            QueryType::Blob as u8,
             DAO_DAOS_COL_PROPOSER_LIMIT,
-            QueryType::Integer as u8,
+            QueryType::Blob as u8,
             DAO_DAOS_COL_QUORUM,
             QueryType::Integer as u8,
             DAO_DAOS_COL_APPROVAL_RATIO_BASE,
@@ -444,8 +540,12 @@ impl Drk {
             let name_bytes: Vec<u8> = serde_json::from_value(row[1].clone())?;
             let name = deserialize(&name_bytes)?;
 
-            let proposer_limit = serde_json::from_value(row[2].clone())?;
-            let quorum = serde_json::from_value(row[3].clone())?;
+            let proposer_limit_bytes: Vec<u8> = serde_json::from_value(row[2].clone())?;
+            let proposer_limit = deserialize(&proposer_limit_bytes)?;
+
+            let quorum_bytes: Vec<u8> = serde_json::from_value(row[3].clone())?;
+            let quorum = deserialize(&quorum_bytes)?;
+
             let approval_ratio_base = serde_json::from_value(row[4].clone())?;
             let approval_ratio_quot = serde_json::from_value(row[5].clone())?;
 
@@ -597,7 +697,9 @@ impl Drk {
 
             let leaf_position_bytes: Vec<u8> = serde_json::from_value(row[7].clone())?;
             let tx_hash_bytes: Vec<u8> = serde_json::from_value(row[8].clone())?;
+
             let call_index = serde_json::from_value(row[9].clone())?;
+
             let vote_id_bytes: Vec<u8> = serde_json::from_value(row[10].clone())?;
 
             let leaf_position = if leaf_position_bytes.is_empty() {
@@ -635,10 +737,179 @@ impl Drk {
         Ok(proposals)
     }
 
+    /// Fetch a DAO proposal by its ID
+    pub async fn get_dao_proposal_by_id(&self, proposal_id: u64) -> Result<DaoProposal> {
+        let query = format!(
+            "SELECT * FROM {} WHERE {} = {}",
+            DAO_PROPOSALS_TABLE, DAO_PROPOSALS_COL_PROPOSAL_ID, proposal_id
+        );
+
+        let params = json!([
+            query,
+            QueryType::Integer as u8,
+            DAO_PROPOSALS_COL_PROPOSAL_ID,
+            QueryType::Integer as u8,
+            DAO_PROPOSALS_COL_DAO_ID,
+            QueryType::Blob as u8,
+            DAO_PROPOSALS_COL_RECV_PUBLIC,
+            QueryType::Blob as u8,
+            DAO_PROPOSALS_COL_AMOUNT,
+            QueryType::Blob as u8,
+            DAO_PROPOSALS_COL_SERIAL,
+            QueryType::Blob as u8,
+            DAO_PROPOSALS_COL_SENDCOIN_TOKEN_ID,
+            QueryType::Blob as u8,
+            DAO_PROPOSALS_COL_BULLA_BLIND,
+            QueryType::OptionBlob as u8,
+            DAO_PROPOSALS_COL_LEAF_POSITION,
+            QueryType::OptionBlob as u8,
+            DAO_PROPOSALS_COL_TX_HASH,
+            QueryType::OptionInteger as u8,
+            DAO_PROPOSALS_COL_CALL_INDEX,
+            QueryType::OptionBlob as u8,
+            DAO_PROPOSALS_COL_OUR_VOTE_ID,
+        ]);
+
+        let req = JsonRequest::new("wallet.query_row_single", params);
+        let rep = self.rpc_client.request(req).await?;
+
+        let Some(row) = rep.as_array() else {
+            return Err(anyhow!("[get_proposal_by_id] Unexpected response from darkfid: {}", rep));
+        };
+
+        let id: u64 = serde_json::from_value(row[0].clone())?;
+        let dao_id: u64 = serde_json::from_value(row[1].clone())?;
+
+        let recipient_bytes: Vec<u8> = serde_json::from_value(row[2].clone())?;
+        let recipient = deserialize(&recipient_bytes)?;
+
+        let amount_bytes: Vec<u8> = serde_json::from_value(row[3].clone())?;
+        let amount = deserialize(&amount_bytes)?;
+
+        let serial_bytes: Vec<u8> = serde_json::from_value(row[4].clone())?;
+        let serial = deserialize(&serial_bytes)?;
+
+        let token_id_bytes: Vec<u8> = serde_json::from_value(row[5].clone())?;
+        let token_id = deserialize(&token_id_bytes)?;
+
+        let bulla_blind_bytes: Vec<u8> = serde_json::from_value(row[6].clone())?;
+        let bulla_blind = deserialize(&bulla_blind_bytes)?;
+
+        let leaf_position_bytes: Vec<u8> = serde_json::from_value(row[7].clone())?;
+        let tx_hash_bytes: Vec<u8> = serde_json::from_value(row[8].clone())?;
+
+        let call_index = serde_json::from_value(row[9].clone())?;
+
+        let vote_id_bytes: Vec<u8> = serde_json::from_value(row[10].clone())?;
+
+        let leaf_position = if leaf_position_bytes.is_empty() {
+            None
+        } else {
+            Some(deserialize(&leaf_position_bytes)?)
+        };
+
+        let tx_hash =
+            if tx_hash_bytes.is_empty() { None } else { Some(deserialize(&tx_hash_bytes)?) };
+
+        let vote_id =
+            if vote_id_bytes.is_empty() { None } else { Some(deserialize(&vote_id_bytes)?) };
+
+        let dao = self.get_dao_by_id(dao_id).await?;
+
+        let proposal = DaoProposal {
+            id,
+            dao_bulla: dao.bulla(),
+            recipient,
+            amount,
+            serial,
+            token_id,
+            bulla_blind,
+            leaf_position,
+            tx_hash,
+            call_index,
+            vote_id,
+        };
+
+        Ok(proposal)
+    }
+
     // 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<Vote>> {
-    //todo!()
-    //}
+    pub async fn get_dao_proposal_votes(&self, proposal_id: u64) -> Result<Vec<DaoVote>> {
+        let query = format!(
+            "SELECT * FROM {} WHERE {} = {}",
+            DAO_VOTES_TABLE, DAO_VOTES_COL_PROPOSAL_ID, proposal_id
+        );
+
+        let params = json!([
+            query,
+            QueryType::Integer as u8,
+            DAO_VOTES_COL_VOTE_ID,
+            QueryType::Integer as u8,
+            DAO_VOTES_COL_PROPOSAL_ID,
+            QueryType::Integer as u8,
+            DAO_VOTES_COL_VOTE_OPTION,
+            QueryType::Blob as u8,
+            DAO_VOTES_COL_YES_VOTE_BLIND,
+            QueryType::Blob as u8,
+            DAO_VOTES_COL_ALL_VOTE_VALUE,
+            QueryType::Blob as u8,
+            DAO_VOTES_COL_ALL_VOTE_BLIND,
+            QueryType::OptionBlob as u8,
+            DAO_VOTES_COL_TX_HASH,
+            QueryType::OptionInteger as u8,
+            DAO_VOTES_COL_CALL_INDEX,
+        ]);
+
+        let req = JsonRequest::new("wallet.query_row_multi", params);
+        let rep = self.rpc_client.request(req).await?;
+
+        let Some(rows) = rep.as_array() else {
+            return Err(anyhow!("[get_dao_proposal_votes] Unexpected response from darkfid: {}", rep));
+        };
+
+        let mut votes = Vec::with_capacity(rows.len());
+
+        for row in rows {
+            let Some(row) = row.as_array() else {
+                return Err(anyhow!("[get_dao_proposal_votes] Unexpected response from darkfid: {}", rep));
+            };
+
+            let id: u64 = serde_json::from_value(row[0].clone())?;
+            let proposal_id: u64 = serde_json::from_value(row[1].clone())?;
+            let vote_option: bool = serde_json::from_value(row[2].clone())?;
+
+            let yes_vote_blind_bytes: Vec<u8> = serde_json::from_value(row[3].clone())?;
+            let yes_vote_blind = deserialize(&yes_vote_blind_bytes)?;
+
+            let all_vote_value_bytes: Vec<u8> = serde_json::from_value(row[4].clone())?;
+            let all_vote_value = deserialize(&all_vote_value_bytes)?;
+
+            let all_vote_blind_bytes: Vec<u8> = serde_json::from_value(row[5].clone())?;
+            let all_vote_blind = deserialize(&all_vote_blind_bytes)?;
+
+            let tx_hash_bytes: Vec<u8> = serde_json::from_value(row[6].clone())?;
+
+            let call_index = serde_json::from_value(row[7].clone())?;
+
+            let tx_hash =
+                if tx_hash_bytes.is_empty() { None } else { Some(deserialize(&tx_hash_bytes)?) };
+
+            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)
+    }
 
     /// Append data related to DAO contract transactions into the wallet database.
     /// Optionally, if `confirm` is true, also append the data in the Merkle trees, etc.
@@ -653,6 +924,9 @@ impl Drk {
         // DAO proposals that have been minted
         let mut new_dao_proposals: Vec<(DaoProposeParams, Option<blake3::Hash>, u32)> = vec![];
         let mut our_proposals: Vec<DaoProposal> = vec![];
+        // DAO votes that have been seen
+        let mut new_dao_votes: Vec<(DaoVoteParams, Option<blake3::Hash>, u32)> = vec![];
+        let mut dao_votes: Vec<DaoVote> = vec![];
 
         // Run through the transaction and see what we got:
         for (i, call) in tx.calls.iter().enumerate() {
@@ -673,12 +947,16 @@ impl Drk {
             }
 
             if call.contract_id == cid && call.data[0] == DaoFunction::Vote as u8 {
-                eprintln!("[UNIMPLEMENTED] Found Dao::Vote in call {}", i);
+                eprintln!("Found Dao::Vote in call {}", i);
+                let params: DaoVoteParams = deserialize(&call.data[1..])?;
+                let tx_hash = if confirm { Some(blake3::hash(&serialize(tx))) } else { None };
+                new_dao_votes.push((params, tx_hash, i as u32));
                 continue
             }
 
             if call.contract_id == cid && call.data[0] == DaoFunction::Exec as u8 {
-                eprintln!("[UNIMPLEMENTED] Found Dao::Exec in call {}", i);
+                // This seems to not need any special action
+                eprintln!("Found Dao::Exec in call {}", i);
                 continue
             }
         }
@@ -743,12 +1021,53 @@ impl Drk {
                     }
                 }
             }
+
+            for vote in new_dao_votes {
+                let enc_note = EncryptedNote2 {
+                    ciphertext: vote.0.ciphertext,
+                    ephem_public: vote.0.ephem_public,
+                };
+
+                for dao in &daos {
+                    if let Ok(note) = enc_note.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 {
+                                proposal_id = Some(i.id);
+                                break
+                            }
+                        }
+
+                        if proposal_id.is_none() {
+                            eprintln!("Warning: Decrypted DaoVoteNote but did not find proposal");
+                            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 confirm {
             self.put_dao_trees(&daos_tree, &proposals_tree).await?;
             self.confirm_daos(&daos_to_confirm).await?;
             self.put_dao_proposals(&our_proposals).await?;
+            self.put_dao_votes(&dao_votes).await?;
         }
 
         Ok(())
@@ -867,4 +1186,47 @@ impl Drk {
 
         Ok(())
     }
+
+    /// Import given DAO votes into the wallet
+    pub async fn put_dao_votes(&self, votes: &[DaoVote]) -> Result<()> {
+        for vote in votes {
+            eprintln!("Importing DAO vote into wallet");
+
+            let query = format!(
+                "INSERT INTO {} ({}, {}, {}, {}, {}, {}, {}) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7);",
+                DAO_VOTES_TABLE,
+                DAO_VOTES_COL_PROPOSAL_ID,
+                DAO_VOTES_COL_VOTE_OPTION,
+                DAO_VOTES_COL_YES_VOTE_BLIND,
+                DAO_VOTES_COL_ALL_VOTE_VALUE,
+                DAO_VOTES_COL_ALL_VOTE_BLIND,
+                DAO_VOTES_COL_TX_HASH,
+                DAO_VOTES_COL_CALL_INDEX,
+            );
+
+            let params = json!([
+                query,
+                QueryType::Integer as u8,
+                vote.proposal_id,
+                QueryType::Integer as u8,
+                vote.vote_option,
+                QueryType::Blob as u8,
+                serialize(&vote.yes_vote_blind),
+                QueryType::Blob as u8,
+                serialize(&vote.all_vote_value),
+                QueryType::Blob as u8,
+                serialize(&vote.all_vote_blind),
+                QueryType::Blob as u8,
+                serialize(&vote.tx_hash.unwrap()),
+                QueryType::Integer as u8,
+                vote.call_index.unwrap(),
+            ]);
+
+            let req = JsonRequest::new("wallet.exec_sql", params);
+            let _ = self.rpc_client.request(req).await?;
+            eprintln!("DAO vote added to wallet");
+        }
+
+        Ok(())
+    }
 }