drk/
dao.rs

1/* This file is part of DarkFi (https://dark.fi)
2 *
3 * Copyright (C) 2020-2025 Dyne.org foundation
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Affero General Public License as
7 * published by the Free Software Foundation, either version 3 of the
8 * License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 */
18
19use std::{collections::HashMap, fmt, str::FromStr};
20
21use lazy_static::lazy_static;
22use num_bigint::BigUint;
23use rand::rngs::OsRng;
24use rusqlite::types::Value;
25
26use darkfi::{
27    tx::{ContractCallLeaf, Transaction, TransactionBuilder},
28    util::{
29        encoding::base64,
30        parse::{decode_base10, encode_base10},
31    },
32    zk::{empty_witnesses, halo2::Field, ProvingKey, ZkCircuit},
33    zkas::ZkBinary,
34    Error, Result,
35};
36use darkfi_dao_contract::{
37    blockwindow,
38    client::{
39        make_mint_call, DaoAuthMoneyTransferCall, DaoExecCall, DaoProposeCall,
40        DaoProposeStakeInput, DaoVoteCall, DaoVoteInput,
41    },
42    model::{
43        Dao, DaoAuthCall, DaoBulla, DaoExecParams, DaoMintParams, DaoProposal, DaoProposalBulla,
44        DaoProposeParams, DaoVoteParams,
45    },
46    DaoFunction, DAO_CONTRACT_ZKAS_DAO_AUTH_MONEY_TRANSFER_ENC_COIN_NS,
47    DAO_CONTRACT_ZKAS_DAO_AUTH_MONEY_TRANSFER_NS, DAO_CONTRACT_ZKAS_DAO_EARLY_EXEC_NS,
48    DAO_CONTRACT_ZKAS_DAO_EXEC_NS, DAO_CONTRACT_ZKAS_DAO_MINT_NS,
49    DAO_CONTRACT_ZKAS_DAO_PROPOSE_INPUT_NS, DAO_CONTRACT_ZKAS_DAO_PROPOSE_MAIN_NS,
50    DAO_CONTRACT_ZKAS_DAO_VOTE_INPUT_NS, DAO_CONTRACT_ZKAS_DAO_VOTE_MAIN_NS,
51};
52use darkfi_money_contract::{
53    client::transfer_v1::{select_coins, TransferCallBuilder, TransferCallInput},
54    model::{CoinAttributes, Nullifier, TokenId},
55    MoneyFunction, MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_FEE_NS_V1,
56    MONEY_CONTRACT_ZKAS_MINT_NS_V1,
57};
58use darkfi_sdk::{
59    bridgetree,
60    crypto::{
61        keypair::{Address, PublicKey, SecretKey, StandardAddress},
62        pasta_prelude::PrimeField,
63        poseidon_hash,
64        smt::{MemoryStorageFp, PoseidonFp, SmtMemoryFp, EMPTY_NODES_FP},
65        util::{fp_mod_fv, fp_to_u64},
66        BaseBlind, Blind, FuncId, FuncRef, MerkleNode, MerkleTree, ScalarBlind, DAO_CONTRACT_ID,
67        MONEY_CONTRACT_ID,
68    },
69    dark_tree::DarkTree,
70    pasta::pallas,
71    tx::TransactionHash,
72    ContractCall,
73};
74use darkfi_serial::{
75    async_trait, deserialize_async, serialize, serialize_async, AsyncEncodable, SerialDecodable,
76    SerialEncodable,
77};
78
79use crate::{
80    cache::{CacheOverlay, CacheSmt, CacheSmtStorage, SLED_MONEY_SMT_TREE},
81    convert_named_params,
82    error::{WalletDbError, WalletDbResult},
83    money::BALANCE_BASE10_DECIMALS,
84    rpc::ScanCache,
85    Drk,
86};
87
88// DAO Merkle trees Sled keys
89pub const SLED_MERKLE_TREES_DAO_DAOS: &[u8] = b"_dao_daos";
90pub const SLED_MERKLE_TREES_DAO_PROPOSALS: &[u8] = b"_dao_proposals";
91
92// Wallet SQL table constant names. These have to represent the `dao.sql`
93// SQL schema. Table names are prefixed with the contract ID to avoid collisions.
94lazy_static! {
95    pub static ref DAO_DAOS_TABLE: String = format!("{}_dao_daos", DAO_CONTRACT_ID.to_string());
96    pub static ref DAO_COINS_TABLE: String = format!("{}_dao_coins", DAO_CONTRACT_ID.to_string());
97    pub static ref DAO_PROPOSALS_TABLE: String =
98        format!("{}_dao_proposals", DAO_CONTRACT_ID.to_string());
99    pub static ref DAO_VOTES_TABLE: String = format!("{}_dao_votes", DAO_CONTRACT_ID.to_string());
100}
101
102// DAO_DAOS_TABLE
103pub const DAO_DAOS_COL_BULLA: &str = "bulla";
104pub const DAO_DAOS_COL_NAME: &str = "name";
105pub const DAO_DAOS_COL_PARAMS: &str = "params";
106pub const DAO_DAOS_COL_LEAF_POSITION: &str = "leaf_position";
107pub const DAO_DAOS_COL_MINT_HEIGHT: &str = "mint_height";
108pub const DAO_DAOS_COL_TX_HASH: &str = "tx_hash";
109pub const DAO_DAOS_COL_CALL_INDEX: &str = "call_index";
110
111// DAO_PROPOSALS_TABLE
112pub const DAO_PROPOSALS_COL_BULLA: &str = "bulla";
113pub const DAO_PROPOSALS_COL_DAO_BULLA: &str = "dao_bulla";
114pub const DAO_PROPOSALS_COL_PROPOSAL: &str = "proposal";
115pub const DAO_PROPOSALS_COL_DATA: &str = "data";
116pub const DAO_PROPOSALS_COL_LEAF_POSITION: &str = "leaf_position";
117pub const DAO_PROPOSALS_COL_MONEY_SNAPSHOT_TREE: &str = "money_snapshot_tree";
118pub const DAO_PROPOSALS_COL_NULLIFIERS_SMT_SNAPSHOT: &str = "nullifiers_smt_snapshot";
119pub const DAO_PROPOSALS_COL_MINT_HEIGHT: &str = "mint_height";
120pub const DAO_PROPOSALS_COL_TX_HASH: &str = "tx_hash";
121pub const DAO_PROPOSALS_COL_CALL_INDEX: &str = "call_index";
122pub const DAO_PROPOSALS_COL_EXEC_HEIGHT: &str = "exec_height";
123pub const DAO_PROPOSALS_COL_EXEC_TX_HASH: &str = "exec_tx_hash";
124
125// DAO_VOTES_TABLE
126pub const DAO_VOTES_COL_PROPOSAL_BULLA: &str = "proposal_bulla";
127pub const DAO_VOTES_COL_VOTE_OPTION: &str = "vote_option";
128pub const DAO_VOTES_COL_YES_VOTE_BLIND: &str = "yes_vote_blind";
129pub const DAO_VOTES_COL_ALL_VOTE_VALUE: &str = "all_vote_value";
130pub const DAO_VOTES_COL_ALL_VOTE_BLIND: &str = "all_vote_blind";
131pub const DAO_VOTES_COL_BLOCK_HEIGHT: &str = "block_height";
132pub const DAO_VOTES_COL_TX_HASH: &str = "tx_hash";
133pub const DAO_VOTES_COL_CALL_INDEX: &str = "call_index";
134pub const DAO_VOTES_COL_NULLIFIERS: &str = "nullifiers";
135
136#[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
137/// Parameters representing a DAO to be initialized
138pub struct DaoParams {
139    /// The on chain representation of the DAO
140    pub dao: Dao,
141    /// DAO notes decryption secret key
142    pub notes_secret_key: Option<SecretKey>,
143    /// DAO proposals creator secret key
144    pub proposer_secret_key: Option<SecretKey>,
145    /// DAO proposals viewer secret key
146    pub proposals_secret_key: Option<SecretKey>,
147    /// DAO votes viewer secret key
148    pub votes_secret_key: Option<SecretKey>,
149    /// DAO proposals executor secret key
150    pub exec_secret_key: Option<SecretKey>,
151    /// DAO strongly supported proposals executor secret key
152    pub early_exec_secret_key: Option<SecretKey>,
153}
154
155impl DaoParams {
156    /// Generate new `DaoParams`. If a specific secret key is provided,
157    /// the corresponding public key will be derived from it and ignore the provided one.
158    #[allow(clippy::too_many_arguments)]
159    pub fn new(
160        proposer_limit: u64,
161        quorum: u64,
162        early_exec_quorum: u64,
163        approval_ratio_base: u64,
164        approval_ratio_quot: u64,
165        gov_token_id: TokenId,
166        notes_secret_key: Option<SecretKey>,
167        notes_public_key: PublicKey,
168        proposer_secret_key: Option<SecretKey>,
169        proposer_public_key: PublicKey,
170        proposals_secret_key: Option<SecretKey>,
171        proposals_public_key: PublicKey,
172        votes_secret_key: Option<SecretKey>,
173        votes_public_key: PublicKey,
174        exec_secret_key: Option<SecretKey>,
175        exec_public_key: PublicKey,
176        early_exec_secret_key: Option<SecretKey>,
177        early_exec_public_key: PublicKey,
178        bulla_blind: BaseBlind,
179    ) -> Self {
180        // Derive corresponding keys from their secret or use the provided ones.
181        let notes_public_key = match notes_secret_key {
182            Some(secret_key) => PublicKey::from_secret(secret_key),
183            None => notes_public_key,
184        };
185        let proposer_public_key = match proposer_secret_key {
186            Some(secret_key) => PublicKey::from_secret(secret_key),
187            None => proposer_public_key,
188        };
189        let proposals_public_key = match proposals_secret_key {
190            Some(secret_key) => PublicKey::from_secret(secret_key),
191            None => proposals_public_key,
192        };
193        let votes_public_key = match votes_secret_key {
194            Some(secret_key) => PublicKey::from_secret(secret_key),
195            None => votes_public_key,
196        };
197        let exec_public_key = match exec_secret_key {
198            Some(secret_key) => PublicKey::from_secret(secret_key),
199            None => exec_public_key,
200        };
201        let early_exec_public_key = match early_exec_secret_key {
202            Some(secret_key) => PublicKey::from_secret(secret_key),
203            None => early_exec_public_key,
204        };
205
206        let dao = Dao {
207            proposer_limit,
208            quorum,
209            early_exec_quorum,
210            approval_ratio_base,
211            approval_ratio_quot,
212            gov_token_id,
213            notes_public_key,
214            proposer_public_key,
215            proposals_public_key,
216            votes_public_key,
217            exec_public_key,
218            early_exec_public_key,
219            bulla_blind,
220        };
221        Self {
222            dao,
223            notes_secret_key,
224            proposer_secret_key,
225            proposals_secret_key,
226            votes_secret_key,
227            exec_secret_key,
228            early_exec_secret_key,
229        }
230    }
231
232    /// Parse provided toml string into `DaoParams`.
233    /// If a specific secret key is provided, the corresponding public key
234    /// will be derived from it and ignore the provided one.
235    pub fn from_toml_str(toml: &str) -> Result<Self> {
236        // Parse TOML file contents
237        let Ok(contents) = toml::from_str::<toml::Value>(toml) else {
238            return Err(Error::ParseFailed("Failed parsing TOML config"))
239        };
240        let Some(table) = contents.as_table() else {
241            return Err(Error::ParseFailed("TOML not a map"))
242        };
243
244        // Grab configuration parameters
245        let Some(proposer_limit) = table.get("proposer_limit") else {
246            return Err(Error::ParseFailed("TOML does not contain proposer limit"))
247        };
248        let Some(proposer_limit) = proposer_limit.as_str() else {
249            return Err(Error::ParseFailed("Invalid proposer limit: Not a string"))
250        };
251        if f64::from_str(proposer_limit).is_err() {
252            return Err(Error::ParseFailed("Invalid proposer limit: Cannot be parsed to float"))
253        }
254        let proposer_limit = decode_base10(proposer_limit, BALANCE_BASE10_DECIMALS, true)?;
255
256        let Some(quorum) = table.get("quorum") else {
257            return Err(Error::ParseFailed("TOML does not contain quorum"))
258        };
259        let Some(quorum) = quorum.as_str() else {
260            return Err(Error::ParseFailed("Invalid quorum: Not a string"))
261        };
262        if f64::from_str(quorum).is_err() {
263            return Err(Error::ParseFailed("Invalid quorum: Cannot be parsed to float"))
264        }
265        let quorum = decode_base10(quorum, BALANCE_BASE10_DECIMALS, true)?;
266
267        let Some(early_exec_quorum) = table.get("early_exec_quorum") else {
268            return Err(Error::ParseFailed("TOML does not contain early exec quorum"))
269        };
270        let Some(early_exec_quorum) = early_exec_quorum.as_str() else {
271            return Err(Error::ParseFailed("Invalid early exec quorum: Not a string"))
272        };
273        if f64::from_str(early_exec_quorum).is_err() {
274            return Err(Error::ParseFailed("Invalid early exec quorum: Cannot be parsed to float"))
275        }
276        let early_exec_quorum = decode_base10(early_exec_quorum, BALANCE_BASE10_DECIMALS, true)?;
277
278        let Some(approval_ratio) = table.get("approval_ratio") else {
279            return Err(Error::ParseFailed("TOML does not contain approval ratio"))
280        };
281        let Some(approval_ratio) = approval_ratio.as_float() else {
282            return Err(Error::ParseFailed("Invalid approval ratio: Not a float"))
283        };
284        if approval_ratio > 1.0 {
285            return Err(Error::ParseFailed("Approval ratio cannot be >1.0"))
286        }
287        let approval_ratio_base = 100_u64;
288        let approval_ratio_quot = (approval_ratio * approval_ratio_base as f64) as u64;
289
290        let Some(gov_token_id) = table.get("gov_token_id") else {
291            return Err(Error::ParseFailed("TOML does not contain gov token id"))
292        };
293        let Some(gov_token_id) = gov_token_id.as_str() else {
294            return Err(Error::ParseFailed("Invalid gov token id: Not a string"))
295        };
296        let gov_token_id = TokenId::from_str(gov_token_id)?;
297
298        let Some(bulla_blind) = table.get("bulla_blind") else {
299            return Err(Error::ParseFailed("TOML does not contain bulla blind"))
300        };
301        let Some(bulla_blind) = bulla_blind.as_str() else {
302            return Err(Error::ParseFailed("Invalid bulla blind: Not a string"))
303        };
304        let bulla_blind = BaseBlind::from_str(bulla_blind)?;
305
306        // Grab DAO actions keypairs
307        let notes_secret_key = match table.get("notes_secret_key") {
308            Some(notes_secret_key) => {
309                let Some(notes_secret_key) = notes_secret_key.as_str() else {
310                    return Err(Error::ParseFailed("Invalid notes secret key: Not a string"))
311                };
312                let Ok(notes_secret_key) = SecretKey::from_str(notes_secret_key) else {
313                    return Err(Error::ParseFailed("Invalid notes secret key: Decoding failed"))
314                };
315                Some(notes_secret_key)
316            }
317            None => None,
318        };
319        let notes_public_key = match notes_secret_key {
320            Some(notes_secret_key) => PublicKey::from_secret(notes_secret_key),
321            None => {
322                let Some(notes_public_key) = table.get("notes_public_key") else {
323                    return Err(Error::ParseFailed("TOML does not contain notes public key"))
324                };
325                let Some(notes_public_key) = notes_public_key.as_str() else {
326                    return Err(Error::ParseFailed("Invalid notes public key: Not a string"))
327                };
328                let Ok(notes_public_key) = PublicKey::from_str(notes_public_key) else {
329                    return Err(Error::ParseFailed("Invalid notes public key: Decoding failed"))
330                };
331                notes_public_key
332            }
333        };
334
335        let proposer_secret_key = match table.get("proposer_secret_key") {
336            Some(proposer_secret_key) => {
337                let Some(proposer_secret_key) = proposer_secret_key.as_str() else {
338                    return Err(Error::ParseFailed("Invalid proposer secret key: Not a string"))
339                };
340                let Ok(proposer_secret_key) = SecretKey::from_str(proposer_secret_key) else {
341                    return Err(Error::ParseFailed("Invalid proposer secret key: Decoding failed"))
342                };
343                Some(proposer_secret_key)
344            }
345            None => None,
346        };
347        let proposer_public_key = match proposer_secret_key {
348            Some(proposer_secret_key) => PublicKey::from_secret(proposer_secret_key),
349            None => {
350                let Some(proposer_public_key) = table.get("proposer_public_key") else {
351                    return Err(Error::ParseFailed("TOML does not contain proposer public key"))
352                };
353                let Some(proposer_public_key) = proposer_public_key.as_str() else {
354                    return Err(Error::ParseFailed("Invalid proposer public key: Not a string"))
355                };
356                let Ok(proposer_public_key) = PublicKey::from_str(proposer_public_key) else {
357                    return Err(Error::ParseFailed("Invalid proposer public key: Decoding failed"))
358                };
359                proposer_public_key
360            }
361        };
362
363        let proposals_secret_key = match table.get("proposals_secret_key") {
364            Some(proposals_secret_key) => {
365                let Some(proposals_secret_key) = proposals_secret_key.as_str() else {
366                    return Err(Error::ParseFailed("Invalid proposals secret key: Not a string"))
367                };
368                let Ok(proposals_secret_key) = SecretKey::from_str(proposals_secret_key) else {
369                    return Err(Error::ParseFailed("Invalid proposals secret key: Decoding failed"))
370                };
371                Some(proposals_secret_key)
372            }
373            None => None,
374        };
375        let proposals_public_key = match proposals_secret_key {
376            Some(proposals_secret_key) => PublicKey::from_secret(proposals_secret_key),
377            None => {
378                let Some(proposals_public_key) = table.get("proposals_public_key") else {
379                    return Err(Error::ParseFailed("TOML does not contain proposals public key"))
380                };
381                let Some(proposals_public_key) = proposals_public_key.as_str() else {
382                    return Err(Error::ParseFailed("Invalid proposals public key: Not a string"))
383                };
384                let Ok(proposals_public_key) = PublicKey::from_str(proposals_public_key) else {
385                    return Err(Error::ParseFailed("Invalid proposals public key: Decoding failed"))
386                };
387                proposals_public_key
388            }
389        };
390
391        let votes_secret_key = match table.get("votes_secret_key") {
392            Some(votes_secret_key) => {
393                let Some(votes_secret_key) = votes_secret_key.as_str() else {
394                    return Err(Error::ParseFailed("Invalid votes secret key: Not a string"))
395                };
396                let Ok(votes_secret_key) = SecretKey::from_str(votes_secret_key) else {
397                    return Err(Error::ParseFailed("Invalid votes secret key: Decoding failed"))
398                };
399                Some(votes_secret_key)
400            }
401            None => None,
402        };
403        let votes_public_key = match votes_secret_key {
404            Some(votes_secret_key) => PublicKey::from_secret(votes_secret_key),
405            None => {
406                let Some(votes_public_key) = table.get("votes_public_key") else {
407                    return Err(Error::ParseFailed("TOML does not contain votes public key"))
408                };
409                let Some(votes_public_key) = votes_public_key.as_str() else {
410                    return Err(Error::ParseFailed("Invalid votes public key: Not a string"))
411                };
412                let Ok(votes_public_key) = PublicKey::from_str(votes_public_key) else {
413                    return Err(Error::ParseFailed("Invalid votes public key: Decoding failed"))
414                };
415                votes_public_key
416            }
417        };
418
419        let exec_secret_key = match table.get("exec_secret_key") {
420            Some(exec_secret_key) => {
421                let Some(exec_secret_key) = exec_secret_key.as_str() else {
422                    return Err(Error::ParseFailed("Invalid exec secret key: Not a string"))
423                };
424                let Ok(exec_secret_key) = SecretKey::from_str(exec_secret_key) else {
425                    return Err(Error::ParseFailed("Invalid exec secret key: Decoding failed"))
426                };
427                Some(exec_secret_key)
428            }
429            None => None,
430        };
431        let exec_public_key = match exec_secret_key {
432            Some(exec_secret_key) => PublicKey::from_secret(exec_secret_key),
433            None => {
434                let Some(exec_public_key) = table.get("exec_public_key") else {
435                    return Err(Error::ParseFailed("TOML does not contain exec public key"))
436                };
437                let Some(exec_public_key) = exec_public_key.as_str() else {
438                    return Err(Error::ParseFailed("Invalid exec public key: Not a string"))
439                };
440                let Ok(exec_public_key) = PublicKey::from_str(exec_public_key) else {
441                    return Err(Error::ParseFailed("Invalid exec public key: Decoding failed"))
442                };
443                exec_public_key
444            }
445        };
446
447        let early_exec_secret_key = match table.get("early_exec_secret_key") {
448            Some(early_exec_secret_key) => {
449                let Some(early_exec_secret_key) = early_exec_secret_key.as_str() else {
450                    return Err(Error::ParseFailed("Invalid early exec secret key: Not a string"))
451                };
452                let Ok(early_exec_secret_key) = SecretKey::from_str(early_exec_secret_key) else {
453                    return Err(Error::ParseFailed("Invalid early exec secret key: Decoding failed"))
454                };
455                Some(early_exec_secret_key)
456            }
457            None => None,
458        };
459        let early_exec_public_key = match early_exec_secret_key {
460            Some(early_exec_secret_key) => PublicKey::from_secret(early_exec_secret_key),
461            None => {
462                let Some(early_exec_public_key) = table.get("early_exec_public_key") else {
463                    return Err(Error::ParseFailed("TOML does not contain early exec public key"))
464                };
465                let Some(early_exec_public_key) = early_exec_public_key.as_str() else {
466                    return Err(Error::ParseFailed("Invalid early exec public key: Not a string"))
467                };
468                let Ok(early_exec_public_key) = PublicKey::from_str(early_exec_public_key) else {
469                    return Err(Error::ParseFailed("Invalid early exec public key: Decoding failed"))
470                };
471                early_exec_public_key
472            }
473        };
474
475        Ok(Self::new(
476            proposer_limit,
477            quorum,
478            early_exec_quorum,
479            approval_ratio_base,
480            approval_ratio_quot,
481            gov_token_id,
482            notes_secret_key,
483            notes_public_key,
484            proposer_secret_key,
485            proposer_public_key,
486            proposals_secret_key,
487            proposals_public_key,
488            votes_secret_key,
489            votes_public_key,
490            exec_secret_key,
491            exec_public_key,
492            early_exec_secret_key,
493            early_exec_public_key,
494            bulla_blind,
495        ))
496    }
497
498    /// Generate a toml string containing the DAO configuration.
499    pub fn toml_str(&self) -> String {
500        // Header comments
501        let mut toml = String::from(
502            "## DAO configuration file\n\
503            ##\n\
504            ## Please make sure you go through all the settings so you can configure\n\
505            ## your DAO properly.\n\
506            ##\n\
507            ## If you want to restrict access to certain actions, the corresponding\n\
508            ## secret key can be omitted. All public keys, along with the DAO configuration\n\
509            ## parameters must be shared.\n\
510            ##\n\
511            ## If you want to combine access to certain actions, you can use the same\n\
512            ## secret and public key combination for them.\n\n",
513        );
514
515        // Configuration parameters
516        toml += &format!(
517            "## ====== DAO configuration parameters =====\n\n\
518            ## The minimum amount of governance tokens needed to open a proposal for this DAO\n\
519            proposer_limit = \"{}\"\n\n\
520            ## Minimal threshold of participating total tokens needed for a proposal to pass\n\
521            quorum = \"{}\"\n\n\
522            ## Minimal threshold of participating total tokens needed for a proposal to\n\
523            ## be considered as strongly supported, enabling early execution.\n\
524            ## Must be greater or equal to normal quorum.\n\
525            early_exec_quorum = \"{}\"\n\n\
526            ## The ratio of winning votes/total votes needed for a proposal to pass (2 decimals)\n\
527            approval_ratio = {}\n\n\
528            ## DAO's governance token ID\n\
529            gov_token_id = \"{}\"\n\n\
530            ## Bulla blind\n\
531            bulla_blind = \"{}\"\n\n",
532            encode_base10(self.dao.proposer_limit, BALANCE_BASE10_DECIMALS),
533            encode_base10(self.dao.quorum, BALANCE_BASE10_DECIMALS),
534            encode_base10(self.dao.early_exec_quorum, BALANCE_BASE10_DECIMALS),
535            self.dao.approval_ratio_quot as f64 / self.dao.approval_ratio_base as f64,
536            self.dao.gov_token_id,
537            self.dao.bulla_blind,
538        );
539
540        // DAO actions keypairs
541        toml += &format!(
542            "## ====== DAO actions keypairs =====\n\n\
543            ## DAO notes decryption keypair\n\
544            notes_public_key = \"{}\"\n",
545            self.dao.notes_public_key,
546        );
547        match self.notes_secret_key {
548            Some(secret_key) => toml += &format!("notes_secret_key = \"{secret_key}\"\n\n"),
549            None => toml += "\n",
550        }
551        toml += &format!(
552            "## DAO proposals creator keypair\n\
553            proposer_public_key = \"{}\"\n",
554            self.dao.proposer_public_key,
555        );
556        match self.proposer_secret_key {
557            Some(secret_key) => toml += &format!("proposer_secret_key = \"{secret_key}\"\n\n"),
558            None => toml += "\n",
559        }
560        toml += &format!(
561            "## DAO proposals viewer keypair\n\
562            proposals_public_key = \"{}\"\n",
563            self.dao.proposals_public_key,
564        );
565        match self.proposals_secret_key {
566            Some(secret_key) => toml += &format!("proposals_secret_key = \"{secret_key}\"\n\n"),
567            None => toml += "\n",
568        }
569        toml += &format!(
570            "## DAO votes viewer keypair\n\
571            votes_public_key = \"{}\"\n",
572            self.dao.votes_public_key,
573        );
574        match self.votes_secret_key {
575            Some(secret_key) => toml += &format!("votes_secret_key = \"{secret_key}\"\n\n"),
576            None => toml += "\n",
577        }
578        toml += &format!(
579            "## DAO proposals executor keypair\n\
580            exec_public_key = \"{}\"\n",
581            self.dao.exec_public_key,
582        );
583        match self.exec_secret_key {
584            Some(secret_key) => toml += &format!("exec_secret_key = \"{secret_key}\"\n\n"),
585            None => toml += "\n",
586        }
587        toml += &format!(
588            "## DAO strongly supported proposals executor keypair\n\
589            early_exec_public_key = \"{}\"",
590            self.dao.early_exec_public_key,
591        );
592        if let Some(secret_key) = self.early_exec_secret_key {
593            toml += &format!("\nearly_exec_secret_key = \"{secret_key}\"")
594        }
595
596        toml
597    }
598}
599
600impl fmt::Display for DaoParams {
601    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
602        // Grab known secret keys
603        let notes_secret_key = match self.notes_secret_key {
604            Some(secret_key) => format!("{secret_key}"),
605            None => "None".to_string(),
606        };
607        let proposer_secret_key = match self.proposer_secret_key {
608            Some(secret_key) => format!("{secret_key}"),
609            None => "None".to_string(),
610        };
611        let proposals_secret_key = match self.proposals_secret_key {
612            Some(secret_key) => format!("{secret_key}"),
613            None => "None".to_string(),
614        };
615        let votes_secret_key = match self.votes_secret_key {
616            Some(secret_key) => format!("{secret_key}"),
617            None => "None".to_string(),
618        };
619        let exec_secret_key = match self.exec_secret_key {
620            Some(secret_key) => format!("{secret_key}"),
621            None => "None".to_string(),
622        };
623        let early_exec_secret_key = match self.early_exec_secret_key {
624            Some(secret_key) => format!("{secret_key}"),
625            None => "None".to_string(),
626        };
627
628        let s = format!(
629            "{}\n{}\n{}: {} ({})\n{}: {} ({})\n{}: {} ({})\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}",
630            "DAO Parameters",
631            "==============",
632            "Proposer limit",
633            encode_base10(self.dao.proposer_limit, BALANCE_BASE10_DECIMALS),
634            self.dao.proposer_limit,
635            "Quorum",
636            encode_base10(self.dao.quorum, BALANCE_BASE10_DECIMALS),
637            self.dao.quorum,
638            "Early Exec Quorum",
639            encode_base10(self.dao.early_exec_quorum, BALANCE_BASE10_DECIMALS),
640            self.dao.early_exec_quorum,
641            "Approval ratio",
642            self.dao.approval_ratio_quot as f64 / self.dao.approval_ratio_base as f64,
643            "Governance Token ID",
644            self.dao.gov_token_id,
645            "Notes Public key",
646            self.dao.notes_public_key,
647            "Notes Secret key",
648            notes_secret_key,
649            "Proposer Public key",
650            self.dao.proposer_public_key,
651            "Proposer Secret key",
652            proposer_secret_key,
653            "Proposals Public key",
654            self.dao.proposals_public_key,
655            "Proposals Secret key",
656            proposals_secret_key,
657            "Votes Public key",
658            self.dao.votes_public_key,
659            "Votes Secret key",
660            votes_secret_key,
661            "Exec Public key",
662            self.dao.exec_public_key,
663            "Exec Secret key",
664            exec_secret_key,
665            "Early Exec Public key",
666            self.dao.early_exec_public_key,
667            "Early Exec Secret key",
668            early_exec_secret_key,
669            "Bulla blind",
670            self.dao.bulla_blind,
671        );
672
673        write!(f, "{s}")
674    }
675}
676
677#[derive(Debug, Clone)]
678/// Structure representing a `DAO_DAOS_TABLE` record.
679pub struct DaoRecord {
680    /// Name identifier for the DAO
681    pub name: String,
682    /// DAO parameters
683    pub params: DaoParams,
684    /// Leaf position of the DAO in the Merkle tree of DAOs
685    pub leaf_position: Option<bridgetree::Position>,
686    /// Block height of the transaction this DAO was deployed
687    pub mint_height: Option<u32>,
688    /// The transaction hash where the DAO was deployed
689    pub tx_hash: Option<TransactionHash>,
690    /// The call index in the transaction where the DAO was deployed
691    pub call_index: Option<u8>,
692}
693
694impl DaoRecord {
695    pub fn new(
696        name: String,
697        params: DaoParams,
698        leaf_position: Option<bridgetree::Position>,
699        mint_height: Option<u32>,
700        tx_hash: Option<TransactionHash>,
701        call_index: Option<u8>,
702    ) -> Self {
703        Self { name, params, leaf_position, mint_height, tx_hash, call_index }
704    }
705
706    pub fn bulla(&self) -> DaoBulla {
707        self.params.dao.to_bulla()
708    }
709}
710
711impl fmt::Display for DaoRecord {
712    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
713        // Grab known secret keys
714        let notes_secret_key = match self.params.notes_secret_key {
715            Some(secret_key) => format!("{secret_key}"),
716            None => "None".to_string(),
717        };
718        let proposer_secret_key = match self.params.proposer_secret_key {
719            Some(secret_key) => format!("{secret_key}"),
720            None => "None".to_string(),
721        };
722        let proposals_secret_key = match self.params.proposals_secret_key {
723            Some(secret_key) => format!("{secret_key}"),
724            None => "None".to_string(),
725        };
726        let votes_secret_key = match self.params.votes_secret_key {
727            Some(secret_key) => format!("{secret_key}"),
728            None => "None".to_string(),
729        };
730        let exec_secret_key = match self.params.exec_secret_key {
731            Some(secret_key) => format!("{secret_key}"),
732            None => "None".to_string(),
733        };
734        let early_exec_secret_key = match self.params.early_exec_secret_key {
735            Some(secret_key) => format!("{secret_key}"),
736            None => "None".to_string(),
737        };
738
739        // Grab mint information
740        let leaf_position = match self.leaf_position {
741            Some(p) => format!("{p:?}"),
742            None => "None".to_string(),
743        };
744        let mint_height = match self.mint_height {
745            Some(h) => format!("{h}"),
746            None => "None".to_string(),
747        };
748        let tx_hash = match self.tx_hash {
749            Some(t) => format!("{t}"),
750            None => "None".to_string(),
751        };
752        let call_index = match self.call_index {
753            Some(c) => format!("{c}"),
754            None => "None".to_string(),
755        };
756
757        let s = format!(
758            "{}\n{}\n{}: {}\n{}: {}\n{}: {} ({})\n{}: {} ({})\n{}: {} ({})\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}",
759            "DAO Parameters",
760            "==============",
761            "Name",
762            self.name,
763            "Bulla",
764            self.bulla(),
765            "Proposer limit",
766            encode_base10(self.params.dao.proposer_limit, BALANCE_BASE10_DECIMALS),
767            self.params.dao.proposer_limit,
768            "Quorum",
769            encode_base10(self.params.dao.quorum, BALANCE_BASE10_DECIMALS),
770            self.params.dao.quorum,
771            "Early Exec Quorum",
772            encode_base10(self.params.dao.early_exec_quorum, BALANCE_BASE10_DECIMALS),
773            self.params.dao.early_exec_quorum,
774            "Approval ratio",
775            self.params.dao.approval_ratio_quot as f64 / self.params.dao.approval_ratio_base as f64,
776            "Governance Token ID",
777            self.params.dao.gov_token_id,
778            "Notes Public key",
779            self.params.dao.notes_public_key,
780            "Notes Secret key",
781            notes_secret_key,
782            "Proposer Public key",
783            self.params.dao.proposer_public_key,
784            "Proposer Secret key",
785            proposer_secret_key,
786            "Proposals Public key",
787            self.params.dao.proposals_public_key,
788            "Proposals Secret key",
789            proposals_secret_key,
790            "Votes Public key",
791            self.params.dao.votes_public_key,
792            "Votes Secret key",
793            votes_secret_key,
794            "Exec Public key",
795            self.params.dao.exec_public_key,
796            "Exec Secret key",
797            exec_secret_key,
798            "Early Exec Public key",
799            self.params.dao.early_exec_public_key,
800            "Early Exec Secret key",
801            early_exec_secret_key,
802            "Bulla blind",
803            self.params.dao.bulla_blind,
804            "Leaf position",
805            leaf_position,
806            "Mint height",
807            mint_height,
808            "Transaction hash",
809            tx_hash,
810            "Call index",
811            call_index,
812        );
813
814        write!(f, "{s}")
815    }
816}
817
818#[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
819/// Structure representing a `DAO_PROPOSALS_TABLE` record.
820pub struct ProposalRecord {
821    /// The on chain representation of the proposal
822    pub proposal: DaoProposal,
823    /// Plaintext proposal call data the members share between them
824    pub data: Option<Vec<u8>>,
825    /// Leaf position of the proposal in the Merkle tree of proposals
826    pub leaf_position: Option<bridgetree::Position>,
827    /// Money merkle tree snapshot for reproducing the snapshot Merkle root
828    pub money_snapshot_tree: Option<MerkleTree>,
829    /// Money nullifiers SMT snapshot for reproducing the snapshot Merkle root
830    pub nullifiers_smt_snapshot: Option<HashMap<BigUint, pallas::Base>>,
831    /// Block height of the transaction this proposal was deployed
832    pub mint_height: Option<u32>,
833    /// The transaction hash where the proposal was deployed
834    pub tx_hash: Option<TransactionHash>,
835    /// The call index in the transaction where the proposal was deployed
836    pub call_index: Option<u8>,
837    /// Block height of the transaction this proposal was executed
838    pub exec_height: Option<u32>,
839    /// The transaction hash where the proposal was executed
840    pub exec_tx_hash: Option<TransactionHash>,
841}
842
843impl ProposalRecord {
844    pub fn bulla(&self) -> DaoProposalBulla {
845        self.proposal.to_bulla()
846    }
847}
848
849impl fmt::Display for ProposalRecord {
850    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
851        let leaf_position = match self.leaf_position {
852            Some(p) => format!("{p:?}"),
853            None => "None".to_string(),
854        };
855        let mint_height = match self.mint_height {
856            Some(h) => format!("{h}"),
857            None => "None".to_string(),
858        };
859        let tx_hash = match self.tx_hash {
860            Some(t) => format!("{t}"),
861            None => "None".to_string(),
862        };
863        let call_index = match self.call_index {
864            Some(c) => format!("{c}"),
865            None => "None".to_string(),
866        };
867
868        let s = format!(
869            "{}\n{}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {} ({})",
870            "Proposal parameters",
871            "===================",
872            "Bulla",
873            self.bulla(),
874            "DAO Bulla",
875            self.proposal.dao_bulla,
876            "Proposal leaf position",
877            leaf_position,
878            "Proposal mint height",
879            mint_height,
880            "Proposal transaction hash",
881            tx_hash,
882            "Proposal call index",
883            call_index,
884            "Creation block window",
885            self.proposal.creation_blockwindow,
886            "Duration",
887            self.proposal.duration_blockwindows,
888            "Block windows"
889        );
890
891        write!(f, "{s}")
892    }
893}
894
895#[derive(Debug, Clone)]
896/// Structure representing a `DAO_VOTES_TABLE` record.
897pub struct VoteRecord {
898    /// Numeric identifier for the vote
899    pub id: u64,
900    /// Bulla identifier of the proposal this vote is for
901    pub proposal: DaoProposalBulla,
902    /// The vote
903    pub vote_option: bool,
904    /// Blinding factor for the yes vote
905    pub yes_vote_blind: ScalarBlind,
906    /// Value of all votes
907    pub all_vote_value: u64,
908    /// Blinding facfor of all votes
909    pub all_vote_blind: ScalarBlind,
910    /// Block height of the transaction this vote was casted
911    pub block_height: u32,
912    /// Transaction hash where this vote was casted
913    pub tx_hash: TransactionHash,
914    /// Call index in the transaction where this vote was casted
915    pub call_index: u8,
916    /// Vote input nullifiers
917    pub nullifiers: Vec<Nullifier>,
918}
919
920impl Drk {
921    /// Initialize wallet with tables for the DAO contract.
922    pub async fn initialize_dao(&self) -> WalletDbResult<()> {
923        // Initialize DAO wallet schema
924        let wallet_schema = include_str!("../dao.sql");
925        self.wallet.exec_batch_sql(wallet_schema)?;
926
927        Ok(())
928    }
929
930    /// Fetch DAO Merkle trees from the wallet.
931    /// If a tree doesn't exists a new Merkle Tree is returned.
932    pub async fn get_dao_trees(&self) -> Result<(MerkleTree, MerkleTree)> {
933        let daos_tree = match self.cache.merkle_trees.get(SLED_MERKLE_TREES_DAO_DAOS)? {
934            Some(tree_bytes) => deserialize_async(&tree_bytes).await?,
935            None => MerkleTree::new(u32::MAX as usize),
936        };
937        let proposals_tree = match self.cache.merkle_trees.get(SLED_MERKLE_TREES_DAO_PROPOSALS)? {
938            Some(tree_bytes) => deserialize_async(&tree_bytes).await?,
939            None => MerkleTree::new(u32::MAX as usize),
940        };
941        Ok((daos_tree, proposals_tree))
942    }
943
944    /// Auxiliary function to parse a `DAO_DAOS_TABLE` record.
945    async fn parse_dao_record(&self, row: &[Value]) -> Result<DaoRecord> {
946        let Value::Text(ref name) = row[1] else {
947            return Err(Error::ParseFailed("[parse_dao_record] Name parsing failed"))
948        };
949        let name = name.clone();
950
951        let Value::Blob(ref params_bytes) = row[2] else {
952            return Err(Error::ParseFailed("[parse_dao_record] Params bytes parsing failed"))
953        };
954        let params = deserialize_async(params_bytes).await?;
955
956        let leaf_position = match row[3] {
957            Value::Blob(ref leaf_position_bytes) => {
958                Some(deserialize_async(leaf_position_bytes).await?)
959            }
960            Value::Null => None,
961            _ => {
962                return Err(Error::ParseFailed(
963                    "[parse_dao_record] Leaf position bytes parsing failed",
964                ))
965            }
966        };
967
968        let mint_height = match row[4] {
969            Value::Integer(mint_height) => {
970                let Ok(mint_height) = u32::try_from(mint_height) else {
971                    return Err(Error::ParseFailed("[parse_dao_record] Mint height parsing failed"))
972                };
973                Some(mint_height)
974            }
975            Value::Null => None,
976            _ => return Err(Error::ParseFailed("[parse_dao_record] Mint height parsing failed")),
977        };
978
979        let tx_hash = match row[5] {
980            Value::Blob(ref tx_hash_bytes) => Some(deserialize_async(tx_hash_bytes).await?),
981            Value::Null => None,
982            _ => {
983                return Err(Error::ParseFailed(
984                    "[parse_dao_record] Transaction hash bytes parsing failed",
985                ))
986            }
987        };
988
989        let call_index = match row[6] {
990            Value::Integer(call_index) => {
991                let Ok(call_index) = u8::try_from(call_index) else {
992                    return Err(Error::ParseFailed("[parse_dao_record] Call index parsing failed"))
993                };
994                Some(call_index)
995            }
996            Value::Null => None,
997            _ => return Err(Error::ParseFailed("[parse_dao_record] Call index parsing failed")),
998        };
999
1000        let dao = DaoRecord::new(name, params, leaf_position, mint_height, tx_hash, call_index);
1001
1002        Ok(dao)
1003    }
1004
1005    /// Fetch all known DAOs from the wallet.
1006    pub async fn get_daos(&self) -> Result<Vec<DaoRecord>> {
1007        let rows = match self.wallet.query_multiple(&DAO_DAOS_TABLE, &[], &[]) {
1008            Ok(r) => r,
1009            Err(e) => {
1010                return Err(Error::DatabaseError(format!("[get_daos] DAOs retrieval failed: {e}")))
1011            }
1012        };
1013
1014        let mut daos = Vec::with_capacity(rows.len());
1015        for row in rows {
1016            daos.push(self.parse_dao_record(&row).await?);
1017        }
1018
1019        Ok(daos)
1020    }
1021
1022    /// Auxiliary function to parse a proposal record row.
1023    async fn parse_dao_proposal(&self, row: &[Value]) -> Result<ProposalRecord> {
1024        let Value::Blob(ref proposal_bytes) = row[2] else {
1025            return Err(Error::ParseFailed(
1026                "[parse_dao_proposal] Proposal bytes bytes parsing failed",
1027            ))
1028        };
1029        let proposal = deserialize_async(proposal_bytes).await?;
1030
1031        let data = match row[3] {
1032            Value::Blob(ref data_bytes) => Some(data_bytes.clone()),
1033            Value::Null => None,
1034            _ => return Err(Error::ParseFailed("[parse_dao_proposal] Data bytes parsing failed")),
1035        };
1036
1037        let leaf_position = match row[4] {
1038            Value::Blob(ref leaf_position_bytes) => {
1039                Some(deserialize_async(leaf_position_bytes).await?)
1040            }
1041            Value::Null => None,
1042            _ => {
1043                return Err(Error::ParseFailed(
1044                    "[parse_dao_proposal] Leaf position bytes parsing failed",
1045                ))
1046            }
1047        };
1048
1049        let money_snapshot_tree = match row[5] {
1050            Value::Blob(ref money_snapshot_tree_bytes) => {
1051                Some(deserialize_async(money_snapshot_tree_bytes).await?)
1052            }
1053            Value::Null => None,
1054            _ => {
1055                return Err(Error::ParseFailed(
1056                    "[parse_dao_proposal] Money snapshot tree bytes parsing failed",
1057                ))
1058            }
1059        };
1060
1061        let nullifiers_smt_snapshot = match row[6] {
1062            Value::Blob(ref nullifiers_smt_snapshot_bytes) => {
1063                Some(deserialize_async(nullifiers_smt_snapshot_bytes).await?)
1064            }
1065            Value::Null => None,
1066            _ => {
1067                return Err(Error::ParseFailed(
1068                    "[parse_dao_proposal] Nullifiers SMT snapshot bytes parsing failed",
1069                ))
1070            }
1071        };
1072
1073        let mint_height = match row[7] {
1074            Value::Integer(mint_height) => {
1075                let Ok(mint_height) = u32::try_from(mint_height) else {
1076                    return Err(Error::ParseFailed(
1077                        "[parse_dao_proposal] Mint height parsing failed",
1078                    ))
1079                };
1080                Some(mint_height)
1081            }
1082            Value::Null => None,
1083            _ => return Err(Error::ParseFailed("[parse_dao_proposal] Mint height parsing failed")),
1084        };
1085
1086        let tx_hash = match row[8] {
1087            Value::Blob(ref tx_hash_bytes) => Some(deserialize_async(tx_hash_bytes).await?),
1088            Value::Null => None,
1089            _ => {
1090                return Err(Error::ParseFailed(
1091                    "[parse_dao_proposal] Transaction hash bytes parsing failed",
1092                ))
1093            }
1094        };
1095
1096        let call_index = match row[9] {
1097            Value::Integer(call_index) => {
1098                let Ok(call_index) = u8::try_from(call_index) else {
1099                    return Err(Error::ParseFailed("[parse_dao_proposal] Call index parsing failed"))
1100                };
1101                Some(call_index)
1102            }
1103            Value::Null => None,
1104            _ => return Err(Error::ParseFailed("[parse_dao_proposal] Call index parsing failed")),
1105        };
1106
1107        let exec_height = match row[10] {
1108            Value::Integer(exec_height) => {
1109                let Ok(exec_height) = u32::try_from(exec_height) else {
1110                    return Err(Error::ParseFailed(
1111                        "[parse_dao_proposal] Execution height parsing failed",
1112                    ))
1113                };
1114                Some(exec_height)
1115            }
1116            Value::Null => None,
1117            _ => {
1118                return Err(Error::ParseFailed(
1119                    "[parse_dao_proposal] Execution height parsing failed",
1120                ))
1121            }
1122        };
1123
1124        let exec_tx_hash = match row[11] {
1125            Value::Blob(ref exec_tx_hash_bytes) => {
1126                Some(deserialize_async(exec_tx_hash_bytes).await?)
1127            }
1128            Value::Null => None,
1129            _ => {
1130                return Err(Error::ParseFailed(
1131                    "[parse_dao_proposal] Execution transaction hash bytes parsing failed",
1132                ))
1133            }
1134        };
1135
1136        Ok(ProposalRecord {
1137            proposal,
1138            data,
1139            leaf_position,
1140            money_snapshot_tree,
1141            nullifiers_smt_snapshot,
1142            mint_height,
1143            tx_hash,
1144            call_index,
1145            exec_height,
1146            exec_tx_hash,
1147        })
1148    }
1149
1150    /// Fetch all known DAO proposals from the wallet given a DAO name.
1151    pub async fn get_dao_proposals(&self, name: &str) -> Result<Vec<ProposalRecord>> {
1152        let Ok(dao) = self.get_dao_by_name(name).await else {
1153            return Err(Error::DatabaseError(format!(
1154                "[get_dao_proposals] DAO with name {name} not found in wallet"
1155            )))
1156        };
1157
1158        let rows = match self.wallet.query_multiple(
1159            &DAO_PROPOSALS_TABLE,
1160            &[],
1161            convert_named_params! {(DAO_PROPOSALS_COL_DAO_BULLA, serialize_async(&dao.bulla()).await)},
1162        ) {
1163            Ok(r) => r,
1164            Err(e) => {
1165                return Err(Error::DatabaseError(format!(
1166                    "[get_dao_proposals] Proposals retrieval failed: {e}"
1167                )))
1168            }
1169        };
1170
1171        let mut proposals = Vec::with_capacity(rows.len());
1172        for row in rows {
1173            let proposal = self.parse_dao_proposal(&row).await?;
1174            proposals.push(proposal);
1175        }
1176
1177        Ok(proposals)
1178    }
1179
1180    /// Auxiliary function to apply `DaoFunction::Mint` call data to
1181    /// the wallet and update the provided scan cache.
1182    /// Returns a flag indicating if the provided call refers to our
1183    /// own wallet.
1184    async fn apply_dao_mint_data(
1185        &self,
1186        scan_cache: &mut ScanCache,
1187        new_bulla: &DaoBulla,
1188        tx_hash: &TransactionHash,
1189        call_index: &u8,
1190        mint_height: &u32,
1191    ) -> Result<bool> {
1192        // Append the new dao bulla to the Merkle tree.
1193        // Every dao bulla has to be added.
1194        scan_cache.dao_daos_tree.append(MerkleNode::from(new_bulla.inner()));
1195
1196        // Check if we have the DAO
1197        if !scan_cache.own_daos.contains_key(new_bulla) {
1198            return Ok(false)
1199        }
1200
1201        // Confirm it
1202        scan_cache.log(format!(
1203            "[apply_dao_mint_data] Found minted DAO {new_bulla}, noting down for wallet update"
1204        ));
1205        if let Err(e) = self
1206            .confirm_dao(
1207                new_bulla,
1208                &scan_cache.dao_daos_tree.mark().unwrap(),
1209                tx_hash,
1210                call_index,
1211                mint_height,
1212            )
1213            .await
1214        {
1215            return Err(Error::DatabaseError(format!(
1216                "[apply_dao_mint_data] Confirm DAO failed: {e}"
1217            )))
1218        }
1219
1220        Ok(true)
1221    }
1222
1223    /// Auxiliary function to apply `DaoFunction::Propose` call data to
1224    /// the wallet and update the provided scan cache.
1225    /// Returns a flag indicating if the provided call refers to our
1226    /// own wallet.
1227    async fn apply_dao_propose_data(
1228        &self,
1229        scan_cache: &mut ScanCache,
1230        params: &DaoProposeParams,
1231        tx_hash: &TransactionHash,
1232        call_index: &u8,
1233        mint_height: &u32,
1234    ) -> Result<bool> {
1235        // Append the new proposal bulla to the Merkle tree.
1236        // Every proposal bulla has to be added.
1237        scan_cache.dao_proposals_tree.append(MerkleNode::from(params.proposal_bulla.inner()));
1238
1239        // If we're able to decrypt this note, that's the way to link it
1240        // to a specific DAO.
1241        for (dao, (proposals_secret_key, _)) in &scan_cache.own_daos {
1242            // Check if we have the proposals key
1243            let Some(proposals_secret_key) = proposals_secret_key else { continue };
1244
1245            // Try to decrypt the proposal note
1246            let Ok(note) = params.note.decrypt::<DaoProposal>(proposals_secret_key) else {
1247                continue
1248            };
1249
1250            // We managed to decrypt it. Let's place this in a proper ProposalRecord object
1251            scan_cache.messages_buffer.push(format!(
1252                "[apply_dao_propose_data] Managed to decrypt proposal note for DAO: {dao}"
1253            ));
1254
1255            // Check if we already got the record
1256            let our_proposal = if scan_cache.own_proposals.contains_key(&params.proposal_bulla) {
1257                // Grab the record from the db
1258                let mut our_proposal =
1259                    self.get_dao_proposal_by_bulla(&params.proposal_bulla).await?;
1260                our_proposal.leaf_position = scan_cache.dao_proposals_tree.mark();
1261                our_proposal.money_snapshot_tree = Some(scan_cache.money_tree.clone());
1262                our_proposal.nullifiers_smt_snapshot = Some(scan_cache.money_smt.store.snapshot()?);
1263                our_proposal.mint_height = Some(*mint_height);
1264                our_proposal.tx_hash = Some(*tx_hash);
1265                our_proposal.call_index = Some(*call_index);
1266                our_proposal
1267            } else {
1268                let our_proposal = ProposalRecord {
1269                    proposal: note,
1270                    data: None,
1271                    leaf_position: scan_cache.dao_proposals_tree.mark(),
1272                    money_snapshot_tree: Some(scan_cache.money_tree.clone()),
1273                    nullifiers_smt_snapshot: Some(scan_cache.money_smt.store.snapshot()?),
1274                    mint_height: Some(*mint_height),
1275                    tx_hash: Some(*tx_hash),
1276                    call_index: Some(*call_index),
1277                    exec_height: None,
1278                    exec_tx_hash: None,
1279                };
1280                scan_cache.own_proposals.insert(params.proposal_bulla, *dao);
1281                our_proposal
1282            };
1283
1284            // Update/store our record
1285            if let Err(e) = self.put_dao_proposal(&our_proposal).await {
1286                return Err(Error::DatabaseError(format!(
1287                    "[apply_dao_propose_data] Put DAO proposals failed: {e}"
1288                )))
1289            }
1290
1291            return Ok(true)
1292        }
1293
1294        Ok(false)
1295    }
1296
1297    /// Auxiliary function to apply `DaoFunction::Vote` call data to
1298    /// the wallet.
1299    /// Returns a flag indicating if the provided call refers to our
1300    /// own wallet.
1301    async fn apply_dao_vote_data(
1302        &self,
1303        scan_cache: &ScanCache,
1304        params: &DaoVoteParams,
1305        tx_hash: &TransactionHash,
1306        call_index: &u8,
1307        block_height: &u32,
1308    ) -> Result<bool> {
1309        // Check if we got the corresponding proposal
1310        let Some(dao_bulla) = scan_cache.own_proposals.get(&params.proposal_bulla) else {
1311            return Ok(false)
1312        };
1313
1314        // Grab the proposal DAO votes key
1315        let Some((_, votes_secret_key)) = scan_cache.own_daos.get(dao_bulla) else {
1316            return Err(Error::DatabaseError(format!(
1317                "[apply_dao_vote_data] Couldn't find proposal {} DAO {}",
1318                params.proposal_bulla, dao_bulla,
1319            )))
1320        };
1321
1322        // Check if we actually have the votes key
1323        let Some(votes_secret_key) = votes_secret_key else { return Ok(false) };
1324
1325        // Decrypt the vote note
1326        let note = match params.note.decrypt_unsafe(votes_secret_key) {
1327            Ok(n) => n,
1328            Err(e) => {
1329                return Err(Error::DatabaseError(format!(
1330                    "[apply_dao_vote_data] Couldn't decrypt proposal {} vote with DAO {} keys: {e}",
1331                    params.proposal_bulla, dao_bulla,
1332                )))
1333            }
1334        };
1335
1336        // Create the DAO vote record
1337        let vote_option = fp_to_u64(note[0]).unwrap();
1338        if vote_option > 1 {
1339            return Err(Error::DatabaseError(format!(
1340                "[apply_dao_vote_data] Malformed vote for proposal {}: {vote_option}",
1341                params.proposal_bulla,
1342            )))
1343        }
1344        let vote_option = vote_option != 0;
1345        let yes_vote_blind = Blind(fp_mod_fv(note[1]));
1346        let all_vote_value = fp_to_u64(note[2]).unwrap();
1347        let all_vote_blind = Blind(fp_mod_fv(note[3]));
1348
1349        let v = VoteRecord {
1350            id: 0, // This will be set by SQLite AUTOINCREMENT
1351            proposal: params.proposal_bulla,
1352            vote_option,
1353            yes_vote_blind,
1354            all_vote_value,
1355            all_vote_blind,
1356            block_height: *block_height,
1357            tx_hash: *tx_hash,
1358            call_index: *call_index,
1359            nullifiers: params.inputs.iter().map(|i| i.vote_nullifier).collect(),
1360        };
1361
1362        if let Err(e) = self.put_dao_vote(&v).await {
1363            return Err(Error::DatabaseError(format!(
1364                "[apply_dao_vote_data] Put DAO votes failed: {e}"
1365            )))
1366        }
1367
1368        Ok(true)
1369    }
1370
1371    /// Auxiliary function to apply `DaoFunction::Exec` call data to
1372    /// the wallet and update the provided scan cache.
1373    /// Returns a flag indicating if the provided call refers to our
1374    /// own wallet.
1375    async fn apply_dao_exec_data(
1376        &self,
1377        scan_cache: &ScanCache,
1378        params: &DaoExecParams,
1379        tx_hash: &TransactionHash,
1380        exec_height: &u32,
1381    ) -> Result<bool> {
1382        // Check if we got the corresponding proposal
1383        if !scan_cache.own_proposals.contains_key(&params.proposal_bulla) {
1384            return Ok(false)
1385        }
1386
1387        // Grab proposal record key
1388        let key = serialize_async(&params.proposal_bulla).await;
1389
1390        // Create an SQL `UPDATE` query to update proposal exec transaction hash
1391        let query = format!(
1392            "UPDATE {} SET {} = ?1, {} = ?2 WHERE {} = ?3;",
1393            *DAO_PROPOSALS_TABLE,
1394            DAO_PROPOSALS_COL_EXEC_HEIGHT,
1395            DAO_PROPOSALS_COL_EXEC_TX_HASH,
1396            DAO_PROPOSALS_COL_BULLA,
1397        );
1398
1399        // Execute the query
1400        if let Err(e) = self
1401            .wallet
1402            .exec_sql(&query, rusqlite::params![Some(*exec_height), Some(serialize(tx_hash)), key])
1403        {
1404            return Err(Error::DatabaseError(format!(
1405                "[apply_dao_exec_data] Update DAO proposal failed: {e}"
1406            )))
1407        }
1408
1409        Ok(true)
1410    }
1411
1412    /// Append data related to DAO contract transactions into the
1413    /// wallet database and update the provided scan cache.
1414    /// Returns a flag indicating if provided data refer to our own
1415    /// wallet.
1416    pub async fn apply_tx_dao_data(
1417        &self,
1418        scan_cache: &mut ScanCache,
1419        data: &[u8],
1420        tx_hash: &TransactionHash,
1421        call_idx: &u8,
1422        block_height: &u32,
1423    ) -> Result<bool> {
1424        // Run through the transaction call data and see what we got:
1425        match DaoFunction::try_from(data[0])? {
1426            DaoFunction::Mint => {
1427                scan_cache.log(String::from("[apply_tx_dao_data] Found Dao::Mint call"));
1428                let params: DaoMintParams = deserialize_async(&data[1..]).await?;
1429                self.apply_dao_mint_data(
1430                    scan_cache,
1431                    &params.dao_bulla,
1432                    tx_hash,
1433                    call_idx,
1434                    block_height,
1435                )
1436                .await
1437            }
1438            DaoFunction::Propose => {
1439                scan_cache.log(String::from("[apply_tx_dao_data] Found Dao::Propose call"));
1440                let params: DaoProposeParams = deserialize_async(&data[1..]).await?;
1441                self.apply_dao_propose_data(scan_cache, &params, tx_hash, call_idx, block_height)
1442                    .await
1443            }
1444            DaoFunction::Vote => {
1445                scan_cache.log(String::from("[apply_tx_dao_data] Found Dao::Vote call"));
1446                let params: DaoVoteParams = deserialize_async(&data[1..]).await?;
1447                self.apply_dao_vote_data(scan_cache, &params, tx_hash, call_idx, block_height).await
1448            }
1449            DaoFunction::Exec => {
1450                scan_cache.log(String::from("[apply_tx_dao_data] Found Dao::Exec call"));
1451                let params: DaoExecParams = deserialize_async(&data[1..]).await?;
1452                self.apply_dao_exec_data(scan_cache, &params, tx_hash, block_height).await
1453            }
1454            DaoFunction::AuthMoneyTransfer => {
1455                scan_cache
1456                    .log(String::from("[apply_tx_dao_data] Found Dao::AuthMoneyTransfer call"));
1457                // Does nothing, just verifies the other calls are correct
1458                Ok(false)
1459            }
1460        }
1461    }
1462
1463    /// Confirm already imported DAO metadata into the wallet.
1464    /// Here we just write the leaf position, mint height, tx hash,
1465    /// and call index.
1466    pub async fn confirm_dao(
1467        &self,
1468        dao: &DaoBulla,
1469        leaf_position: &bridgetree::Position,
1470        tx_hash: &TransactionHash,
1471        call_index: &u8,
1472        mint_height: &u32,
1473    ) -> WalletDbResult<()> {
1474        // Grab dao record key
1475        let key = serialize_async(dao).await;
1476
1477        // Create an SQL `UPDATE` query
1478        let query = format!(
1479            "UPDATE {} SET {} = ?1, {} = ?2, {} = ?3, {} = ?4 WHERE {} = ?5;",
1480            *DAO_DAOS_TABLE,
1481            DAO_DAOS_COL_LEAF_POSITION,
1482            DAO_DAOS_COL_MINT_HEIGHT,
1483            DAO_DAOS_COL_TX_HASH,
1484            DAO_DAOS_COL_CALL_INDEX,
1485            DAO_DAOS_COL_BULLA
1486        );
1487
1488        // Create its params
1489        let params = rusqlite::params![
1490            serialize(leaf_position),
1491            Some(*mint_height),
1492            serialize(tx_hash),
1493            call_index,
1494            key,
1495        ];
1496
1497        // Execute the query
1498        self.wallet.exec_sql(&query, params)
1499    }
1500
1501    /// Import given DAO proposal into the wallet.
1502    pub async fn put_dao_proposal(&self, proposal: &ProposalRecord) -> Result<()> {
1503        // Check that we already have the proposal DAO
1504        if let Err(e) = self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await {
1505            return Err(Error::DatabaseError(format!(
1506                "[put_dao_proposal] Couldn't find proposal {} DAO {}: {e}",
1507                proposal.bulla(),
1508                proposal.proposal.dao_bulla
1509            )))
1510        }
1511
1512        // Grab proposal record key
1513        let key = serialize_async(&proposal.bulla()).await;
1514
1515        // Create an SQL `INSERT OR REPLACE` query
1516        let query = format!(
1517            "INSERT OR REPLACE INTO {} ({}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12);",
1518            *DAO_PROPOSALS_TABLE,
1519            DAO_PROPOSALS_COL_BULLA,
1520            DAO_PROPOSALS_COL_DAO_BULLA,
1521            DAO_PROPOSALS_COL_PROPOSAL,
1522            DAO_PROPOSALS_COL_DATA,
1523            DAO_PROPOSALS_COL_LEAF_POSITION,
1524            DAO_PROPOSALS_COL_MONEY_SNAPSHOT_TREE,
1525            DAO_PROPOSALS_COL_NULLIFIERS_SMT_SNAPSHOT,
1526            DAO_PROPOSALS_COL_MINT_HEIGHT,
1527            DAO_PROPOSALS_COL_TX_HASH,
1528            DAO_PROPOSALS_COL_CALL_INDEX,
1529            DAO_PROPOSALS_COL_EXEC_HEIGHT,
1530            DAO_PROPOSALS_COL_EXEC_TX_HASH,
1531        );
1532
1533        // Create its params
1534        let data = match &proposal.data {
1535            Some(data) => Some(data),
1536            None => None,
1537        };
1538
1539        let leaf_position = match &proposal.leaf_position {
1540            Some(leaf_position) => Some(serialize_async(leaf_position).await),
1541            None => None,
1542        };
1543
1544        let money_snapshot_tree = match &proposal.money_snapshot_tree {
1545            Some(money_snapshot_tree) => Some(serialize_async(money_snapshot_tree).await),
1546            None => None,
1547        };
1548
1549        let nullifiers_smt_snapshot = match &proposal.nullifiers_smt_snapshot {
1550            Some(nullifiers_smt_snapshot) => Some(serialize_async(nullifiers_smt_snapshot).await),
1551            None => None,
1552        };
1553
1554        let tx_hash = match &proposal.tx_hash {
1555            Some(tx_hash) => Some(serialize_async(tx_hash).await),
1556            None => None,
1557        };
1558
1559        let exec_tx_hash = match &proposal.exec_tx_hash {
1560            Some(exec_tx_hash) => Some(serialize_async(exec_tx_hash).await),
1561            None => None,
1562        };
1563
1564        let params = rusqlite::params![
1565            key,
1566            serialize(&proposal.proposal.dao_bulla),
1567            serialize(&proposal.proposal),
1568            data,
1569            leaf_position,
1570            money_snapshot_tree,
1571            nullifiers_smt_snapshot,
1572            proposal.mint_height,
1573            tx_hash,
1574            proposal.call_index,
1575            proposal.exec_height,
1576            exec_tx_hash,
1577        ];
1578
1579        // Execute the query
1580        if let Err(e) = self.wallet.exec_sql(&query, params) {
1581            return Err(Error::DatabaseError(format!(
1582                "[put_dao_proposal] Proposal insert failed: {e}"
1583            )))
1584        }
1585
1586        Ok(())
1587    }
1588
1589    /// Import given DAO vote into the wallet.
1590    pub async fn put_dao_vote(&self, vote: &VoteRecord) -> WalletDbResult<()> {
1591        // Create an SQL `INSERT OR REPLACE` query
1592        let query = format!(
1593            "INSERT INTO {} ({}, {}, {}, {}, {}, {}, {}, {}, {}) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9);",
1594            *DAO_VOTES_TABLE,
1595            DAO_VOTES_COL_PROPOSAL_BULLA,
1596            DAO_VOTES_COL_VOTE_OPTION,
1597            DAO_VOTES_COL_YES_VOTE_BLIND,
1598            DAO_VOTES_COL_ALL_VOTE_VALUE,
1599            DAO_VOTES_COL_ALL_VOTE_BLIND,
1600            DAO_VOTES_COL_BLOCK_HEIGHT,
1601            DAO_VOTES_COL_TX_HASH,
1602            DAO_VOTES_COL_CALL_INDEX,
1603            DAO_VOTES_COL_NULLIFIERS,
1604        );
1605
1606        // Create its params
1607        let params = rusqlite::params![
1608            serialize(&vote.proposal),
1609            vote.vote_option as u64,
1610            serialize(&vote.yes_vote_blind),
1611            serialize(&vote.all_vote_value),
1612            serialize(&vote.all_vote_blind),
1613            vote.block_height,
1614            serialize(&vote.tx_hash),
1615            vote.call_index,
1616            serialize(&vote.nullifiers),
1617        ];
1618
1619        // Execute the query
1620        self.wallet.exec_sql(&query, params)?;
1621
1622        Ok(())
1623    }
1624
1625    /// Reset the DAO Merkle trees in the cache.
1626    pub fn reset_dao_trees(&self, output: &mut Vec<String>) -> WalletDbResult<()> {
1627        output.push(String::from("Resetting DAO Merkle trees"));
1628        if let Err(e) = self.cache.merkle_trees.remove(SLED_MERKLE_TREES_DAO_DAOS) {
1629            output.push(format!("[reset_dao_trees] Resetting DAO DAOs Merkle tree failed: {e}"));
1630            return Err(WalletDbError::GenericError)
1631        }
1632        if let Err(e) = self.cache.merkle_trees.remove(SLED_MERKLE_TREES_DAO_PROPOSALS) {
1633            output
1634                .push(format!("[reset_dao_trees] Resetting DAO Proposals Merkle tree failed: {e}"));
1635            return Err(WalletDbError::GenericError)
1636        }
1637        output.push(String::from("Successfully reset DAO Merkle trees"));
1638
1639        Ok(())
1640    }
1641
1642    /// Reset confirmed DAOs in the wallet.
1643    pub fn reset_daos(&self, output: &mut Vec<String>) -> WalletDbResult<()> {
1644        output.push(String::from("Resetting DAO confirmations"));
1645        let query = format!(
1646            "UPDATE {} SET {} = NULL, {} = NULL, {} = NULL, {} = NULL;",
1647            *DAO_DAOS_TABLE,
1648            DAO_DAOS_COL_LEAF_POSITION,
1649            DAO_DAOS_COL_MINT_HEIGHT,
1650            DAO_DAOS_COL_TX_HASH,
1651            DAO_DAOS_COL_CALL_INDEX,
1652        );
1653        self.wallet.exec_sql(&query, &[])?;
1654        output.push(String::from("Successfully unconfirmed DAOs"));
1655
1656        Ok(())
1657    }
1658
1659    /// Reset confirmed DAOs in the wallet that were minted after
1660    /// provided height.
1661    pub fn unconfirm_daos_after(
1662        &self,
1663        height: &u32,
1664        output: &mut Vec<String>,
1665    ) -> WalletDbResult<()> {
1666        output.push(format!("Resetting DAO confirmations after: {height}"));
1667        let query = format!(
1668            "UPDATE {} SET {} = NULL, {} = NULL, {} = NULL, {} = NULL WHERE {} > ?1;",
1669            *DAO_DAOS_TABLE,
1670            DAO_DAOS_COL_LEAF_POSITION,
1671            DAO_DAOS_COL_MINT_HEIGHT,
1672            DAO_DAOS_COL_TX_HASH,
1673            DAO_DAOS_COL_CALL_INDEX,
1674            DAO_DAOS_COL_MINT_HEIGHT,
1675        );
1676        self.wallet.exec_sql(&query, rusqlite::params![Some(*height)])?;
1677        output.push(String::from("Successfully unconfirmed DAOs"));
1678
1679        Ok(())
1680    }
1681
1682    /// Reset all DAO proposals in the wallet.
1683    pub fn reset_dao_proposals(&self, output: &mut Vec<String>) -> WalletDbResult<()> {
1684        output.push(String::from("Resetting DAO proposals confirmations"));
1685        let query = format!(
1686            "UPDATE {} SET {} = NULL, {} = NULL, {} = NULL, {} = NULL, {} = NULL, {} = NULL, {} = NULL, {} = NULL;",
1687            *DAO_PROPOSALS_TABLE,
1688            DAO_PROPOSALS_COL_LEAF_POSITION,
1689            DAO_PROPOSALS_COL_MONEY_SNAPSHOT_TREE,
1690            DAO_PROPOSALS_COL_NULLIFIERS_SMT_SNAPSHOT,
1691            DAO_PROPOSALS_COL_MINT_HEIGHT,
1692            DAO_PROPOSALS_COL_TX_HASH,
1693            DAO_PROPOSALS_COL_CALL_INDEX,
1694            DAO_PROPOSALS_COL_EXEC_HEIGHT,
1695            DAO_PROPOSALS_COL_EXEC_TX_HASH,
1696        );
1697        self.wallet.exec_sql(&query, &[])?;
1698        output.push(String::from("Successfully unconfirmed DAO proposals"));
1699
1700        Ok(())
1701    }
1702
1703    /// Reset DAO proposals in the wallet that were minted after
1704    /// provided height.
1705    pub fn unconfirm_dao_proposals_after(
1706        &self,
1707        height: &u32,
1708        output: &mut Vec<String>,
1709    ) -> WalletDbResult<()> {
1710        output.push(format!("Resetting DAO proposals confirmations after: {height}"));
1711        let query = format!(
1712            "UPDATE {} SET {} = NULL, {} = NULL, {} = NULL, {} = NULL, {} = NULL, {} = NULL, {} = NULL, {} = NULL WHERE {} > ?1;",
1713            *DAO_PROPOSALS_TABLE,
1714            DAO_PROPOSALS_COL_LEAF_POSITION,
1715            DAO_PROPOSALS_COL_MONEY_SNAPSHOT_TREE,
1716            DAO_PROPOSALS_COL_NULLIFIERS_SMT_SNAPSHOT,
1717            DAO_PROPOSALS_COL_MINT_HEIGHT,
1718            DAO_PROPOSALS_COL_TX_HASH,
1719            DAO_PROPOSALS_COL_CALL_INDEX,
1720            DAO_PROPOSALS_COL_EXEC_HEIGHT,
1721            DAO_PROPOSALS_COL_EXEC_TX_HASH,
1722            DAO_PROPOSALS_COL_MINT_HEIGHT,
1723        );
1724        self.wallet.exec_sql(&query, rusqlite::params![Some(*height)])?;
1725        output.push(String::from("Successfully unconfirmed DAO proposals"));
1726
1727        Ok(())
1728    }
1729
1730    /// Reset execution information in the wallet for DAO proposals
1731    /// that were executed after provided height.
1732    pub fn unexec_dao_proposals_after(
1733        &self,
1734        height: &u32,
1735        output: &mut Vec<String>,
1736    ) -> WalletDbResult<()> {
1737        output.push(format!("Resetting DAO proposals execution information after: {height}"));
1738        let query = format!(
1739            "UPDATE {} SET {} = NULL, {} = NULL WHERE {} > ?1;",
1740            *DAO_PROPOSALS_TABLE,
1741            DAO_PROPOSALS_COL_EXEC_HEIGHT,
1742            DAO_PROPOSALS_COL_EXEC_TX_HASH,
1743            DAO_PROPOSALS_COL_EXEC_HEIGHT,
1744        );
1745        self.wallet.exec_sql(&query, rusqlite::params![Some(*height)])?;
1746        output.push(String::from("Successfully reset DAO proposals execution information"));
1747
1748        Ok(())
1749    }
1750
1751    /// Reset all DAO votes in the wallet.
1752    pub fn reset_dao_votes(&self, output: &mut Vec<String>) -> WalletDbResult<()> {
1753        output.push(String::from("Resetting DAO votes"));
1754        let query = format!("DELETE FROM {};", *DAO_VOTES_TABLE);
1755        self.wallet.exec_sql(&query, &[])?;
1756        output.push(String::from("Successfully reset DAO votes"));
1757
1758        Ok(())
1759    }
1760
1761    /// Remove the DAO votes in the wallet that were created after
1762    /// provided height.
1763    pub fn remove_dao_votes_after(
1764        &self,
1765        height: &u32,
1766        output: &mut Vec<String>,
1767    ) -> WalletDbResult<()> {
1768        output.push(format!("Removing DAO votes after: {height}"));
1769        let query =
1770            format!("DELETE FROM {} WHERE {} > ?1;", *DAO_VOTES_TABLE, DAO_VOTES_COL_BLOCK_HEIGHT);
1771        self.wallet.exec_sql(&query, rusqlite::params![height])?;
1772        output.push(String::from("Successfully removed DAO votes"));
1773
1774        Ok(())
1775    }
1776
1777    /// Import given DAO params into the wallet with a given name.
1778    pub async fn import_dao(
1779        &self,
1780        name: &str,
1781        params: &DaoParams,
1782        output: &mut Vec<String>,
1783    ) -> Result<()> {
1784        // Grab the params DAO
1785        let bulla = params.dao.to_bulla();
1786
1787        // Check if we already have imported the DAO so we retain its
1788        // mint information.
1789        match self.get_dao_by_bulla(&bulla).await {
1790            Ok(dao) => {
1791                output.push(format!("Updating \"{}\" DAO keys into the wallet", dao.name));
1792                let query = format!(
1793                    "UPDATE {} SET {} = ?1 WHERE {} = ?2;",
1794                    *DAO_DAOS_TABLE, DAO_DAOS_COL_PARAMS, DAO_DAOS_COL_BULLA,
1795                );
1796                if let Err(e) =
1797                    self.wallet.exec_sql(
1798                        &query,
1799                        rusqlite::params![
1800                            serialize_async(params).await,
1801                            serialize_async(&bulla).await,
1802                        ],
1803                    )
1804                {
1805                    return Err(Error::DatabaseError(format!("[import_dao] DAO update failed: {e}")))
1806                };
1807            }
1808            Err(_) => {
1809                output.push(format!("Importing \"{name}\" DAO into the wallet"));
1810                let query = format!(
1811                    "INSERT INTO {} ({}, {}, {}) VALUES (?1, ?2, ?3);",
1812                    *DAO_DAOS_TABLE, DAO_DAOS_COL_BULLA, DAO_DAOS_COL_NAME, DAO_DAOS_COL_PARAMS,
1813                );
1814                if let Err(e) = self.wallet.exec_sql(
1815                    &query,
1816                    rusqlite::params![
1817                        serialize_async(&params.dao.to_bulla()).await,
1818                        name,
1819                        serialize_async(params).await,
1820                    ],
1821                ) {
1822                    return Err(Error::DatabaseError(format!("[import_dao] DAO insert failed: {e}")))
1823                };
1824            }
1825        };
1826
1827        Ok(())
1828    }
1829
1830    /// Fetch a DAO given its bulla.
1831    pub async fn get_dao_by_bulla(&self, bulla: &DaoBulla) -> Result<DaoRecord> {
1832        let row = match self.wallet.query_single(
1833            &DAO_DAOS_TABLE,
1834            &[],
1835            convert_named_params! {(DAO_DAOS_COL_BULLA, serialize_async(bulla).await)},
1836        ) {
1837            Ok(r) => r,
1838            Err(e) => {
1839                return Err(Error::DatabaseError(format!(
1840                    "[get_dao_by_bulla] DAO retrieval failed: {e}"
1841                )))
1842            }
1843        };
1844
1845        self.parse_dao_record(&row).await
1846    }
1847
1848    /// Fetch a DAO given its name.
1849    pub async fn get_dao_by_name(&self, name: &str) -> Result<DaoRecord> {
1850        let row = match self.wallet.query_single(
1851            &DAO_DAOS_TABLE,
1852            &[],
1853            convert_named_params! {(DAO_DAOS_COL_NAME, name)},
1854        ) {
1855            Ok(r) => r,
1856            Err(e) => {
1857                return Err(Error::DatabaseError(format!(
1858                    "[get_dao_by_name] DAO retrieval failed: {e}"
1859                )))
1860            }
1861        };
1862
1863        self.parse_dao_record(&row).await
1864    }
1865
1866    /// List DAO(s) imported in the wallet. If a name is given, just print the
1867    /// metadata for that specific one, if found.
1868    pub async fn dao_list(&self, name: &Option<String>, output: &mut Vec<String>) -> Result<()> {
1869        if let Some(name) = name {
1870            let dao = self.get_dao_by_name(name).await?;
1871            output.push(format!("{dao}"));
1872            let address: Address =
1873                StandardAddress::from_public(self.network, dao.params.dao.notes_public_key).into();
1874            output.push(format!("Wallet Address: {address}"));
1875            return Ok(());
1876        }
1877
1878        let daos = self.get_daos().await?;
1879        for (i, dao) in daos.iter().enumerate() {
1880            output.push(format!("{i}. {}", dao.name));
1881        }
1882
1883        Ok(())
1884    }
1885
1886    /// Fetch known unspent balances from the wallet for the given DAO name.
1887    pub async fn dao_balance(&self, name: &str) -> Result<HashMap<String, u64>> {
1888        let dao = self.get_dao_by_name(name).await?;
1889
1890        let dao_spend_hook =
1891            FuncRef { contract_id: *DAO_CONTRACT_ID, func_code: DaoFunction::Exec as u8 }
1892                .to_func_id();
1893
1894        let mut coins = self.get_coins(false).await?;
1895        coins.retain(|x| x.0.note.spend_hook == dao_spend_hook);
1896        coins.retain(|x| x.0.note.user_data == dao.bulla().inner());
1897
1898        // Fill this map with balances
1899        let mut balmap: HashMap<String, u64> = HashMap::new();
1900
1901        for coin in coins {
1902            let mut value = coin.0.note.value;
1903
1904            if let Some(prev) = balmap.get(&coin.0.note.token_id.to_string()) {
1905                value += prev;
1906            }
1907
1908            balmap.insert(coin.0.note.token_id.to_string(), value);
1909        }
1910
1911        Ok(balmap)
1912    }
1913
1914    /// Fetch known unspent balances from the wallet for the given DAO name.
1915    pub async fn dao_mining_config(&self, name: &str, output: &mut Vec<String>) -> Result<()> {
1916        let dao = self.get_dao_by_name(name).await?;
1917        let address: Address =
1918            StandardAddress::from_public(self.network, dao.params.dao.notes_public_key).into();
1919        let recipient = address.to_string();
1920        let spend_hook = format!(
1921            "{}",
1922            FuncRef { contract_id: *DAO_CONTRACT_ID, func_code: DaoFunction::Exec as u8 }
1923                .to_func_id()
1924        );
1925        let user_data = bs58::encode(dao.bulla().inner().to_repr()).into_string();
1926        output.push(String::from("DarkFi TOML configuration:"));
1927        output.push(format!("recipient = \"{recipient}\""));
1928        output.push(format!("spend_hook = \"{spend_hook}\""));
1929        output.push(format!("user_data = \"{user_data}\""));
1930        output.push(String::from("\nP2Pool wallet address to use:"));
1931        output.push(
1932            base64::encode(&serialize(&(recipient, Some(spend_hook), Some(user_data)))).to_string(),
1933        );
1934
1935        Ok(())
1936    }
1937
1938    /// Fetch all known DAO proposalss from the wallet.
1939    pub async fn get_proposals(&self) -> Result<Vec<ProposalRecord>> {
1940        let rows = match self.wallet.query_multiple(&DAO_PROPOSALS_TABLE, &[], &[]) {
1941            Ok(r) => r,
1942            Err(e) => {
1943                return Err(Error::DatabaseError(format!(
1944                    "[get_proposals] DAO proposalss retrieval failed: {e}"
1945                )))
1946            }
1947        };
1948
1949        let mut daos = Vec::with_capacity(rows.len());
1950        for row in rows {
1951            daos.push(self.parse_dao_proposal(&row).await?);
1952        }
1953
1954        Ok(daos)
1955    }
1956
1957    /// Fetch a DAO proposal by its bulla.
1958    pub async fn get_dao_proposal_by_bulla(
1959        &self,
1960        bulla: &DaoProposalBulla,
1961    ) -> Result<ProposalRecord> {
1962        // Grab the proposal record
1963        let row = match self.wallet.query_single(
1964            &DAO_PROPOSALS_TABLE,
1965            &[],
1966            convert_named_params! {(DAO_PROPOSALS_COL_BULLA, serialize_async(bulla).await)},
1967        ) {
1968            Ok(r) => r,
1969            Err(e) => {
1970                return Err(Error::DatabaseError(format!(
1971                    "[get_dao_proposal_by_bulla] DAO proposal retrieval failed: {e}"
1972                )))
1973            }
1974        };
1975
1976        // Parse rest of the record
1977        self.parse_dao_proposal(&row).await
1978    }
1979
1980    // Fetch all known DAO proposal votes from the wallet given a proposal ID.
1981    pub async fn get_dao_proposal_votes(
1982        &self,
1983        proposal: &DaoProposalBulla,
1984    ) -> Result<Vec<VoteRecord>> {
1985        let rows = match self.wallet.query_multiple(
1986            &DAO_VOTES_TABLE,
1987            &[],
1988            convert_named_params! {(DAO_VOTES_COL_PROPOSAL_BULLA, serialize_async(proposal).await)},
1989        ) {
1990            Ok(r) => r,
1991            Err(e) => {
1992                return Err(Error::DatabaseError(format!(
1993                    "[get_dao_proposal_votes] Votes retrieval failed: {e}"
1994                )))
1995            }
1996        };
1997
1998        let mut votes = Vec::with_capacity(rows.len());
1999        for row in rows {
2000            let Value::Integer(id) = row[0] else {
2001                return Err(Error::ParseFailed("[get_dao_proposal_votes] ID parsing failed"))
2002            };
2003            let Ok(id) = u64::try_from(id) else {
2004                return Err(Error::ParseFailed("[get_dao_proposal_votes] ID parsing failed"))
2005            };
2006
2007            let Value::Blob(ref proposal_bytes) = row[1] else {
2008                return Err(Error::ParseFailed(
2009                    "[get_dao_proposal_votes] Proposal bytes bytes parsing failed",
2010                ))
2011            };
2012            let proposal = deserialize_async(proposal_bytes).await?;
2013
2014            let Value::Integer(vote_option) = row[2] else {
2015                return Err(Error::ParseFailed(
2016                    "[get_dao_proposal_votes] Vote option parsing failed",
2017                ))
2018            };
2019            let Ok(vote_option) = u32::try_from(vote_option) else {
2020                return Err(Error::ParseFailed(
2021                    "[get_dao_proposal_votes] Vote option parsing failed",
2022                ))
2023            };
2024            let vote_option = vote_option != 0;
2025
2026            let Value::Blob(ref yes_vote_blind_bytes) = row[3] else {
2027                return Err(Error::ParseFailed(
2028                    "[get_dao_proposal_votes] Yes vote blind bytes parsing failed",
2029                ))
2030            };
2031            let yes_vote_blind = deserialize_async(yes_vote_blind_bytes).await?;
2032
2033            let Value::Blob(ref all_vote_value_bytes) = row[4] else {
2034                return Err(Error::ParseFailed(
2035                    "[get_dao_proposal_votes] All vote value bytes parsing failed",
2036                ))
2037            };
2038            let all_vote_value = deserialize_async(all_vote_value_bytes).await?;
2039
2040            let Value::Blob(ref all_vote_blind_bytes) = row[5] else {
2041                return Err(Error::ParseFailed(
2042                    "[get_dao_proposal_votes] All vote blind bytes parsing failed",
2043                ))
2044            };
2045            let all_vote_blind = deserialize_async(all_vote_blind_bytes).await?;
2046
2047            let Value::Integer(block_height) = row[6] else {
2048                return Err(Error::ParseFailed(
2049                    "[get_dao_proposal_votes] Block height parsing failed",
2050                ))
2051            };
2052            let Ok(block_height) = u32::try_from(block_height) else {
2053                return Err(Error::ParseFailed(
2054                    "[get_dao_proposal_votes] Block height parsing failed",
2055                ))
2056            };
2057
2058            let Value::Blob(ref tx_hash_bytes) = row[7] else {
2059                return Err(Error::ParseFailed(
2060                    "[get_dao_proposal_votes] Transaction hash bytes parsing failed",
2061                ))
2062            };
2063            let tx_hash = deserialize_async(tx_hash_bytes).await?;
2064
2065            let Value::Integer(call_index) = row[8] else {
2066                return Err(Error::ParseFailed("[get_dao_proposal_votes] Call index parsing failed"))
2067            };
2068            let Ok(call_index) = u8::try_from(call_index) else {
2069                return Err(Error::ParseFailed("[get_dao_proposal_votes] Call index parsing failed"))
2070            };
2071
2072            let Value::Blob(ref nullifiers_bytes) = row[9] else {
2073                return Err(Error::ParseFailed(
2074                    "[get_dao_proposal_votes] Nullifiers bytes parsing failed",
2075                ))
2076            };
2077            let nullifiers = deserialize_async(nullifiers_bytes).await?;
2078
2079            let vote = VoteRecord {
2080                id,
2081                proposal,
2082                vote_option,
2083                yes_vote_blind,
2084                all_vote_value,
2085                all_vote_blind,
2086                block_height,
2087                tx_hash,
2088                call_index,
2089                nullifiers,
2090            };
2091
2092            votes.push(vote);
2093        }
2094
2095        Ok(votes)
2096    }
2097
2098    /// Mint a DAO on-chain.
2099    pub async fn dao_mint(&self, name: &str) -> Result<Transaction> {
2100        // Retrieve the dao record
2101        let dao = self.get_dao_by_name(name).await?;
2102
2103        // Check its not already minted
2104        if dao.tx_hash.is_some() {
2105            return Err(Error::Custom(
2106                "[dao_mint] This DAO seems to have already been minted on-chain".to_string(),
2107            ))
2108        }
2109
2110        // Check that we have all the keys
2111        if dao.params.notes_secret_key.is_none() ||
2112            dao.params.proposer_secret_key.is_none() ||
2113            dao.params.proposals_secret_key.is_none() ||
2114            dao.params.votes_secret_key.is_none() ||
2115            dao.params.exec_secret_key.is_none() ||
2116            dao.params.early_exec_secret_key.is_none()
2117        {
2118            return Err(Error::Custom(
2119                "[dao_mint] We need all the secrets key to mint the DAO on-chain".to_string(),
2120            ))
2121        }
2122
2123        // Now we need to do a lookup for the zkas proof bincodes, and create
2124        // the circuit objects and proving keys so we can build the transaction.
2125        // We also do this through the RPC. First we grab the fee call from money.
2126        let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
2127
2128        let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1)
2129        else {
2130            return Err(Error::Custom("Fee circuit not found".to_string()))
2131        };
2132
2133        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
2134
2135        let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
2136
2137        // Creating Fee circuit proving key
2138        let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
2139
2140        // Now we grab the DAO mint
2141        let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?;
2142
2143        let Some(dao_mint_zkbin) = zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_MINT_NS)
2144        else {
2145            return Err(Error::DatabaseError("[dao_mint] DAO Mint circuit not found".to_string()))
2146        };
2147
2148        let dao_mint_zkbin = ZkBinary::decode(&dao_mint_zkbin.1)?;
2149
2150        let dao_mint_circuit = ZkCircuit::new(empty_witnesses(&dao_mint_zkbin)?, &dao_mint_zkbin);
2151
2152        // Creating DAO Mint circuit proving key
2153        let dao_mint_pk = ProvingKey::build(dao_mint_zkbin.k, &dao_mint_circuit);
2154
2155        // Create the DAO mint call
2156        let notes_secret_key = dao.params.notes_secret_key.unwrap();
2157        let (params, proofs) = make_mint_call(
2158            &dao.params.dao,
2159            &notes_secret_key,
2160            &dao.params.proposer_secret_key.unwrap(),
2161            &dao.params.proposals_secret_key.unwrap(),
2162            &dao.params.votes_secret_key.unwrap(),
2163            &dao.params.exec_secret_key.unwrap(),
2164            &dao.params.early_exec_secret_key.unwrap(),
2165            &dao_mint_zkbin,
2166            &dao_mint_pk,
2167        )?;
2168        let mut data = vec![DaoFunction::Mint as u8];
2169        params.encode_async(&mut data).await?;
2170        let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
2171
2172        // Create the TransactionBuilder containing above call
2173        let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?;
2174
2175        // We first have to execute the fee-less tx to gather its used gas, and then we feed
2176        // it into the fee-creating function.
2177        let mut tx = tx_builder.build()?;
2178        let sigs = tx.create_sigs(&[notes_secret_key])?;
2179        tx.signatures.push(sigs);
2180
2181        let tree = self.get_money_tree().await?;
2182        let (fee_call, fee_proofs, fee_secrets) =
2183            self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?;
2184
2185        // Append the fee call to the transaction
2186        tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
2187
2188        // Now build the actual transaction and sign it with all necessary keys.
2189        let mut tx = tx_builder.build()?;
2190        let sigs = tx.create_sigs(&[notes_secret_key])?;
2191        tx.signatures.push(sigs);
2192        let sigs = tx.create_sigs(&fee_secrets)?;
2193        tx.signatures.push(sigs);
2194
2195        Ok(tx)
2196    }
2197
2198    /// Create a DAO transfer proposal.
2199    #[allow(clippy::too_many_arguments)]
2200    pub async fn dao_propose_transfer(
2201        &self,
2202        name: &str,
2203        duration_blockwindows: u64,
2204        amount: &str,
2205        token_id: TokenId,
2206        recipient: PublicKey,
2207        spend_hook: Option<FuncId>,
2208        user_data: Option<pallas::Base>,
2209    ) -> Result<ProposalRecord> {
2210        // Fetch DAO and check its deployed
2211        let dao = self.get_dao_by_name(name).await?;
2212        if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() {
2213            return Err(Error::Custom(
2214                "[dao_propose_transfer] DAO seems to not have been deployed yet".to_string(),
2215            ))
2216        }
2217
2218        // Check that we have the proposer key
2219        if dao.params.proposer_secret_key.is_none() {
2220            return Err(Error::Custom(
2221                "[dao_propose_transfer] We need the proposer secret key to create proposals for this DAO".to_string(),
2222            ))
2223        }
2224
2225        // Fetch DAO unspent OwnCoins to see what its balance is
2226        let dao_spend_hook =
2227            FuncRef { contract_id: *DAO_CONTRACT_ID, func_code: DaoFunction::Exec as u8 }
2228                .to_func_id();
2229        let dao_bulla = dao.bulla();
2230        let dao_owncoins =
2231            self.get_contract_token_coins(&token_id, &dao_spend_hook, &dao_bulla.inner()).await?;
2232        if dao_owncoins.is_empty() {
2233            return Err(Error::Custom(format!(
2234                "[dao_propose_transfer] Did not find any {token_id} unspent coins owned by this DAO"
2235            )))
2236        }
2237
2238        // Check DAO balance is sufficient
2239        let amount = decode_base10(amount, BALANCE_BASE10_DECIMALS, false)?;
2240        if dao_owncoins.iter().map(|x| x.note.value).sum::<u64>() < amount {
2241            return Err(Error::Custom(format!(
2242                "[dao_propose_transfer] Not enough DAO balance for token ID: {token_id}",
2243            )))
2244        }
2245
2246        // Generate proposal coin attributes
2247        let proposal_coinattrs = CoinAttributes {
2248            public_key: recipient,
2249            value: amount,
2250            token_id,
2251            spend_hook: spend_hook.unwrap_or(FuncId::none()),
2252            user_data: user_data.unwrap_or(pallas::Base::ZERO),
2253            blind: Blind::random(&mut OsRng),
2254        };
2255
2256        // Convert coin_params to actual coins
2257        let proposal_coins = vec![proposal_coinattrs.to_coin()];
2258        let mut proposal_data = vec![];
2259        proposal_coins.encode_async(&mut proposal_data).await?;
2260
2261        // Create Auth calls
2262        let auth_calls = vec![
2263            DaoAuthCall {
2264                contract_id: *DAO_CONTRACT_ID,
2265                function_code: DaoFunction::AuthMoneyTransfer as u8,
2266                auth_data: proposal_data,
2267            },
2268            DaoAuthCall {
2269                contract_id: *MONEY_CONTRACT_ID,
2270                function_code: MoneyFunction::TransferV1 as u8,
2271                auth_data: vec![],
2272            },
2273        ];
2274
2275        // Retrieve next block height and current block time target,
2276        // to compute their window.
2277        let next_block_height = self.get_next_block_height().await?;
2278        let block_target = self.get_block_target().await?;
2279        let creation_blockwindow = blockwindow(next_block_height, block_target);
2280
2281        // Create the actual proposal
2282        let proposal = DaoProposal {
2283            auth_calls,
2284            creation_blockwindow,
2285            duration_blockwindows,
2286            user_data: user_data.unwrap_or(pallas::Base::ZERO),
2287            dao_bulla,
2288            blind: Blind::random(&mut OsRng),
2289        };
2290
2291        let proposal_record = ProposalRecord {
2292            proposal,
2293            data: Some(serialize_async(&proposal_coinattrs).await),
2294            leaf_position: None,
2295            money_snapshot_tree: None,
2296            nullifiers_smt_snapshot: None,
2297            mint_height: None,
2298            tx_hash: None,
2299            call_index: None,
2300            exec_height: None,
2301            exec_tx_hash: None,
2302        };
2303
2304        if let Err(e) = self.put_dao_proposal(&proposal_record).await {
2305            return Err(Error::DatabaseError(format!(
2306                "[dao_propose_transfer] Put DAO proposal failed: {e}"
2307            )))
2308        }
2309
2310        Ok(proposal_record)
2311    }
2312
2313    /// Create a DAO generic proposal.
2314    pub async fn dao_propose_generic(
2315        &self,
2316        name: &str,
2317        duration_blockwindows: u64,
2318        user_data: Option<pallas::Base>,
2319    ) -> Result<ProposalRecord> {
2320        // Fetch DAO and check its deployed
2321        let dao = self.get_dao_by_name(name).await?;
2322        if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() {
2323            return Err(Error::Custom(
2324                "[dao_propose_generic] DAO seems to not have been deployed yet".to_string(),
2325            ))
2326        }
2327
2328        // Check that we have the proposer key
2329        if dao.params.proposer_secret_key.is_none() {
2330            return Err(Error::Custom(
2331                "[dao_propose_generic] We need the proposer secret key to create proposals for this DAO".to_string(),
2332            ))
2333        }
2334
2335        // Retrieve next block height and current block time target,
2336        // to compute their window.
2337        let next_block_height = self.get_next_block_height().await?;
2338        let block_target = self.get_block_target().await?;
2339        let creation_blockwindow = blockwindow(next_block_height, block_target);
2340
2341        // Create the actual proposal
2342        let proposal = DaoProposal {
2343            auth_calls: vec![],
2344            creation_blockwindow,
2345            duration_blockwindows,
2346            user_data: user_data.unwrap_or(pallas::Base::ZERO),
2347            dao_bulla: dao.bulla(),
2348            blind: Blind::random(&mut OsRng),
2349        };
2350
2351        let proposal_record = ProposalRecord {
2352            proposal,
2353            data: None,
2354            leaf_position: None,
2355            money_snapshot_tree: None,
2356            nullifiers_smt_snapshot: None,
2357            mint_height: None,
2358            tx_hash: None,
2359            call_index: None,
2360            exec_height: None,
2361            exec_tx_hash: None,
2362        };
2363
2364        if let Err(e) = self.put_dao_proposal(&proposal_record).await {
2365            return Err(Error::DatabaseError(format!(
2366                "[dao_propose_generic] Put DAO proposal failed: {e}"
2367            )))
2368        }
2369
2370        Ok(proposal_record)
2371    }
2372
2373    /// Create a DAO transfer proposal transaction.
2374    pub async fn dao_transfer_proposal_tx(&self, proposal: &ProposalRecord) -> Result<Transaction> {
2375        // Check we know the plaintext data
2376        if proposal.data.is_none() {
2377            return Err(Error::Custom(
2378                "[dao_transfer_proposal_tx] Proposal plainext data is empty".to_string(),
2379            ))
2380        }
2381        let proposal_coinattrs: CoinAttributes =
2382            deserialize_async(proposal.data.as_ref().unwrap()).await?;
2383
2384        // Fetch DAO and check its deployed
2385        let Ok(dao) = self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await else {
2386            return Err(Error::Custom(format!(
2387                "[dao_transfer_proposal_tx] DAO {} was not found",
2388                proposal.proposal.dao_bulla
2389            )))
2390        };
2391        if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() {
2392            return Err(Error::Custom(
2393                "[dao_transfer_proposal_tx] DAO seems to not have been deployed yet".to_string(),
2394            ))
2395        }
2396
2397        // Check that we have the proposer key
2398        if dao.params.proposer_secret_key.is_none() {
2399            return Err(Error::Custom(
2400                "[dao_transfer_proposal_tx] We need the proposer secret key to create proposals for this DAO".to_string(),
2401            ))
2402        }
2403
2404        // Fetch DAO unspent OwnCoins to see what its balance is for the coin
2405        let dao_spend_hook =
2406            FuncRef { contract_id: *DAO_CONTRACT_ID, func_code: DaoFunction::Exec as u8 }
2407                .to_func_id();
2408        let dao_owncoins = self
2409            .get_contract_token_coins(
2410                &proposal_coinattrs.token_id,
2411                &dao_spend_hook,
2412                &proposal.proposal.dao_bulla.inner(),
2413            )
2414            .await?;
2415        if dao_owncoins.is_empty() {
2416            return Err(Error::Custom(format!(
2417                "[dao_transfer_proposal_tx] Did not find any {} unspent coins owned by this DAO",
2418                proposal_coinattrs.token_id,
2419            )))
2420        }
2421
2422        // Check DAO balance is sufficient
2423        if dao_owncoins.iter().map(|x| x.note.value).sum::<u64>() < proposal_coinattrs.value {
2424            return Err(Error::Custom(format!(
2425                "[dao_transfer_proposal_tx] Not enough DAO balance for token ID: {}",
2426                proposal_coinattrs.token_id,
2427            )))
2428        }
2429
2430        // Fetch our own governance OwnCoins to see what our balance is
2431        let gov_owncoins = self.get_token_coins(&dao.params.dao.gov_token_id).await?;
2432        if gov_owncoins.is_empty() {
2433            return Err(Error::Custom(format!(
2434                "[dao_transfer_proposal_tx] Did not find any governance {} coins in wallet",
2435                dao.params.dao.gov_token_id
2436            )))
2437        }
2438
2439        // Find which governance coins we can use
2440        let mut total_value = 0;
2441        let mut gov_owncoins_to_use = vec![];
2442        for gov_owncoin in gov_owncoins {
2443            if total_value >= dao.params.dao.proposer_limit {
2444                break
2445            }
2446
2447            total_value += gov_owncoin.note.value;
2448            gov_owncoins_to_use.push(gov_owncoin);
2449        }
2450
2451        // Check our governance coins balance is sufficient
2452        if total_value < dao.params.dao.proposer_limit {
2453            return Err(Error::Custom(format!(
2454                "[dao_transfer_proposal_tx] Not enough gov token {} balance to propose",
2455                dao.params.dao.gov_token_id
2456            )))
2457        }
2458
2459        // Now we need to do a lookup for the zkas proof bincodes, and create
2460        // the circuit objects and proving keys so we can build the transaction.
2461        // We also do this through the RPC. First we grab the fee call from money.
2462        let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
2463
2464        let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1)
2465        else {
2466            return Err(Error::Custom(
2467                "[dao_transfer_proposal_tx] Fee circuit not found".to_string(),
2468            ))
2469        };
2470
2471        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
2472
2473        let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
2474
2475        // Creating Fee circuit proving key
2476        let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
2477
2478        // Now we grab the DAO bins
2479        let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?;
2480
2481        let Some(propose_burn_zkbin) =
2482            zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_PROPOSE_INPUT_NS)
2483        else {
2484            return Err(Error::Custom(
2485                "[dao_transfer_proposal_tx] Propose Burn circuit not found".to_string(),
2486            ))
2487        };
2488
2489        let Some(propose_main_zkbin) =
2490            zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_PROPOSE_MAIN_NS)
2491        else {
2492            return Err(Error::Custom(
2493                "[dao_transfer_proposal_tx] Propose Main circuit not found".to_string(),
2494            ))
2495        };
2496
2497        let propose_burn_zkbin = ZkBinary::decode(&propose_burn_zkbin.1)?;
2498        let propose_main_zkbin = ZkBinary::decode(&propose_main_zkbin.1)?;
2499
2500        let propose_burn_circuit =
2501            ZkCircuit::new(empty_witnesses(&propose_burn_zkbin)?, &propose_burn_zkbin);
2502        let propose_main_circuit =
2503            ZkCircuit::new(empty_witnesses(&propose_main_zkbin)?, &propose_main_zkbin);
2504
2505        // Creating DAO ProposeBurn and ProposeMain circuits proving keys
2506        let propose_burn_pk = ProvingKey::build(propose_burn_zkbin.k, &propose_burn_circuit);
2507        let propose_main_pk = ProvingKey::build(propose_main_zkbin.k, &propose_main_circuit);
2508
2509        // Fetch our money Merkle tree
2510        let money_merkle_tree = self.get_money_tree().await?;
2511
2512        // Now we can create the proposal transaction parameters.
2513        // We first generate the `DaoProposeStakeInput` inputs,
2514        // using our governance OwnCoins.
2515        let mut inputs = Vec::with_capacity(gov_owncoins_to_use.len());
2516        for gov_owncoin in gov_owncoins_to_use {
2517            let input = DaoProposeStakeInput {
2518                secret: gov_owncoin.secret,
2519                note: gov_owncoin.note.clone(),
2520                leaf_position: gov_owncoin.leaf_position,
2521                merkle_path: money_merkle_tree.witness(gov_owncoin.leaf_position, 0).unwrap(),
2522            };
2523            inputs.push(input);
2524        }
2525
2526        // Now create the parameters for the proposal tx.
2527        // Fetch the daos Merkle tree to compute the DAO Merkle path and root.
2528        let (daos_tree, _) = self.get_dao_trees().await?;
2529        let (dao_merkle_path, dao_merkle_root) = {
2530            let root = daos_tree.root(0).unwrap();
2531            let leaf_pos = dao.leaf_position.unwrap();
2532            let dao_merkle_path = daos_tree.witness(leaf_pos, 0).unwrap();
2533            (dao_merkle_path, root)
2534        };
2535
2536        // Generate the Money nullifiers Sparse Merkle Tree
2537        let store = CacheSmtStorage::new(CacheOverlay::new(&self.cache)?, SLED_MONEY_SMT_TREE);
2538        let money_null_smt = CacheSmt::new(store, PoseidonFp::new(), &EMPTY_NODES_FP);
2539
2540        // Create the proposal call
2541        let call = DaoProposeCall {
2542            money_null_smt: &money_null_smt,
2543            inputs,
2544            proposal: proposal.proposal.clone(),
2545            dao: dao.params.dao,
2546            dao_leaf_position: dao.leaf_position.unwrap(),
2547            dao_merkle_path,
2548            dao_merkle_root,
2549        };
2550
2551        let (params, proofs, signature_secrets) = call.make(
2552            &dao.params.proposer_secret_key.unwrap(),
2553            &propose_burn_zkbin,
2554            &propose_burn_pk,
2555            &propose_main_zkbin,
2556            &propose_main_pk,
2557        )?;
2558
2559        // Encode the call
2560        let mut data = vec![DaoFunction::Propose as u8];
2561        params.encode_async(&mut data).await?;
2562        let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
2563
2564        // Create the TransactionBuilder containing above call
2565        let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?;
2566
2567        // We first have to execute the fee-less tx to gather its used gas, and then we feed
2568        // it into the fee-creating function.
2569        let mut tx = tx_builder.build()?;
2570        let sigs = tx.create_sigs(&signature_secrets)?;
2571        tx.signatures.push(sigs);
2572
2573        let tree = self.get_money_tree().await?;
2574        let (fee_call, fee_proofs, fee_secrets) =
2575            self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?;
2576
2577        // Append the fee call to the transaction
2578        tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
2579
2580        // Now build the actual transaction and sign it with all necessary keys.
2581        let mut tx = tx_builder.build()?;
2582        let sigs = tx.create_sigs(&signature_secrets)?;
2583        tx.signatures.push(sigs);
2584        let sigs = tx.create_sigs(&fee_secrets)?;
2585        tx.signatures.push(sigs);
2586
2587        Ok(tx)
2588    }
2589
2590    /// Create a DAO generic proposal transaction.
2591    pub async fn dao_generic_proposal_tx(&self, proposal: &ProposalRecord) -> Result<Transaction> {
2592        // Fetch DAO and check its deployed
2593        let Ok(dao) = self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await else {
2594            return Err(Error::Custom(format!(
2595                "[dao_generic_proposal_tx] DAO {} was not found",
2596                proposal.proposal.dao_bulla
2597            )))
2598        };
2599        if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() {
2600            return Err(Error::Custom(
2601                "[dao_generic_proposal_tx] DAO seems to not have been deployed yet".to_string(),
2602            ))
2603        }
2604
2605        // Check that we have the proposer key
2606        if dao.params.proposer_secret_key.is_none() {
2607            return Err(Error::Custom(
2608                "[dao_generic_proposal_tx] We need the proposer secret key to create proposals for this DAO".to_string(),
2609            ))
2610        }
2611
2612        // Fetch our own governance OwnCoins to see what our balance is
2613        let gov_owncoins = self.get_token_coins(&dao.params.dao.gov_token_id).await?;
2614        if gov_owncoins.is_empty() {
2615            return Err(Error::Custom(format!(
2616                "[dao_generic_proposal_tx] Did not find any governance {} coins in wallet",
2617                dao.params.dao.gov_token_id
2618            )))
2619        }
2620
2621        // Find which governance coins we can use
2622        let mut total_value = 0;
2623        let mut gov_owncoins_to_use = vec![];
2624        for gov_owncoin in gov_owncoins {
2625            if total_value >= dao.params.dao.proposer_limit {
2626                break
2627            }
2628
2629            total_value += gov_owncoin.note.value;
2630            gov_owncoins_to_use.push(gov_owncoin);
2631        }
2632
2633        // Check our governance coins balance is sufficient
2634        if total_value < dao.params.dao.proposer_limit {
2635            return Err(Error::Custom(format!(
2636                "[dao_generic_proposal_tx] Not enough gov token {} balance to propose",
2637                dao.params.dao.gov_token_id
2638            )))
2639        }
2640
2641        // Now we need to do a lookup for the zkas proof bincodes, and create
2642        // the circuit objects and proving keys so we can build the transaction.
2643        // We also do this through the RPC. First we grab the fee call from money.
2644        let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
2645
2646        let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1)
2647        else {
2648            return Err(Error::Custom("[dao_generic_proposal_tx] Fee circuit not found".to_string()))
2649        };
2650
2651        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
2652
2653        let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
2654
2655        // Creating Fee circuit proving key
2656        let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
2657
2658        // Now we grab the DAO bins
2659        let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?;
2660
2661        let Some(propose_burn_zkbin) =
2662            zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_PROPOSE_INPUT_NS)
2663        else {
2664            return Err(Error::Custom(
2665                "[dao_generic_proposal_tx] Propose Burn circuit not found".to_string(),
2666            ))
2667        };
2668
2669        let Some(propose_main_zkbin) =
2670            zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_PROPOSE_MAIN_NS)
2671        else {
2672            return Err(Error::Custom(
2673                "[dao_generic_proposal_tx] Propose Main circuit not found".to_string(),
2674            ))
2675        };
2676
2677        let propose_burn_zkbin = ZkBinary::decode(&propose_burn_zkbin.1)?;
2678        let propose_main_zkbin = ZkBinary::decode(&propose_main_zkbin.1)?;
2679
2680        let propose_burn_circuit =
2681            ZkCircuit::new(empty_witnesses(&propose_burn_zkbin)?, &propose_burn_zkbin);
2682        let propose_main_circuit =
2683            ZkCircuit::new(empty_witnesses(&propose_main_zkbin)?, &propose_main_zkbin);
2684
2685        // Creating DAO ProposeBurn and ProposeMain circuits proving keys
2686        let propose_burn_pk = ProvingKey::build(propose_burn_zkbin.k, &propose_burn_circuit);
2687        let propose_main_pk = ProvingKey::build(propose_main_zkbin.k, &propose_main_circuit);
2688
2689        // Fetch our money Merkle tree
2690        let money_merkle_tree = self.get_money_tree().await?;
2691
2692        // Now we can create the proposal transaction parameters.
2693        // We first generate the `DaoProposeStakeInput` inputs,
2694        // using our governance OwnCoins.
2695        let mut inputs = Vec::with_capacity(gov_owncoins_to_use.len());
2696        for gov_owncoin in gov_owncoins_to_use {
2697            let input = DaoProposeStakeInput {
2698                secret: gov_owncoin.secret,
2699                note: gov_owncoin.note.clone(),
2700                leaf_position: gov_owncoin.leaf_position,
2701                merkle_path: money_merkle_tree.witness(gov_owncoin.leaf_position, 0).unwrap(),
2702            };
2703            inputs.push(input);
2704        }
2705
2706        // Now create the parameters for the proposal tx.
2707        // Fetch the daos Merkle tree to compute the DAO Merkle path and root.
2708        let (daos_tree, _) = self.get_dao_trees().await?;
2709        let (dao_merkle_path, dao_merkle_root) = {
2710            let root = daos_tree.root(0).unwrap();
2711            let leaf_pos = dao.leaf_position.unwrap();
2712            let dao_merkle_path = daos_tree.witness(leaf_pos, 0).unwrap();
2713            (dao_merkle_path, root)
2714        };
2715
2716        // Generate the Money nullifiers Sparse Merkle Tree
2717        let store = CacheSmtStorage::new(CacheOverlay::new(&self.cache)?, SLED_MONEY_SMT_TREE);
2718        let money_null_smt = CacheSmt::new(store, PoseidonFp::new(), &EMPTY_NODES_FP);
2719
2720        // Create the proposal call
2721        let call = DaoProposeCall {
2722            money_null_smt: &money_null_smt,
2723            inputs,
2724            proposal: proposal.proposal.clone(),
2725            dao: dao.params.dao,
2726            dao_leaf_position: dao.leaf_position.unwrap(),
2727            dao_merkle_path,
2728            dao_merkle_root,
2729        };
2730
2731        let (params, proofs, signature_secrets) = call.make(
2732            &dao.params.proposer_secret_key.unwrap(),
2733            &propose_burn_zkbin,
2734            &propose_burn_pk,
2735            &propose_main_zkbin,
2736            &propose_main_pk,
2737        )?;
2738
2739        // Encode the call
2740        let mut data = vec![DaoFunction::Propose as u8];
2741        params.encode_async(&mut data).await?;
2742        let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
2743
2744        // Create the TransactionBuilder containing above call
2745        let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?;
2746
2747        // We first have to execute the fee-less tx to gather its used gas, and then we feed
2748        // it into the fee-creating function.
2749        let mut tx = tx_builder.build()?;
2750        let sigs = tx.create_sigs(&signature_secrets)?;
2751        tx.signatures.push(sigs);
2752
2753        let tree = self.get_money_tree().await?;
2754        let (fee_call, fee_proofs, fee_secrets) =
2755            self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?;
2756
2757        // Append the fee call to the transaction
2758        tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
2759
2760        // Now build the actual transaction and sign it with all necessary keys.
2761        let mut tx = tx_builder.build()?;
2762        let sigs = tx.create_sigs(&signature_secrets)?;
2763        tx.signatures.push(sigs);
2764        let sigs = tx.create_sigs(&fee_secrets)?;
2765        tx.signatures.push(sigs);
2766
2767        Ok(tx)
2768    }
2769
2770    /// Vote on a DAO proposal
2771    pub async fn dao_vote(
2772        &self,
2773        proposal_bulla: &DaoProposalBulla,
2774        vote_option: bool,
2775        weight: Option<u64>,
2776    ) -> Result<Transaction> {
2777        // Feth the proposal and check its deployed
2778        let Ok(proposal) = self.get_dao_proposal_by_bulla(proposal_bulla).await else {
2779            return Err(Error::Custom(format!("[dao_vote] Proposal {proposal_bulla} was not found")))
2780        };
2781        if proposal.leaf_position.is_none() ||
2782            proposal.money_snapshot_tree.is_none() ||
2783            proposal.nullifiers_smt_snapshot.is_none() ||
2784            proposal.tx_hash.is_none() ||
2785            proposal.call_index.is_none()
2786        {
2787            return Err(Error::Custom(
2788                "[dao_vote] Proposal seems to not have been deployed yet".to_string(),
2789            ))
2790        }
2791
2792        // Check proposal is not executed
2793        if let Some(exec_tx_hash) = proposal.exec_tx_hash {
2794            return Err(Error::Custom(format!(
2795                "[dao_vote] Proposal was executed on transaction: {exec_tx_hash}"
2796            )))
2797        }
2798
2799        // Fetch DAO and check its deployed
2800        let Ok(dao) = self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await else {
2801            return Err(Error::Custom(format!(
2802                "[dao_vote] DAO {} was not found",
2803                proposal.proposal.dao_bulla
2804            )))
2805        };
2806        if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() {
2807            return Err(Error::Custom(
2808                "[dao_vote] DAO seems to not have been deployed yet".to_string(),
2809            ))
2810        }
2811
2812        // Fetch all the proposal votes to check for duplicate nullifiers
2813        let votes = self.get_dao_proposal_votes(proposal_bulla).await?;
2814        let mut votes_nullifiers = vec![];
2815        for vote in votes {
2816            for nullifier in vote.nullifiers {
2817                if !votes_nullifiers.contains(&nullifier) {
2818                    votes_nullifiers.push(nullifier);
2819                }
2820            }
2821        }
2822
2823        // Fetch our own governance OwnCoins to see what our balance is
2824        let gov_owncoins = self.get_token_coins(&dao.params.dao.gov_token_id).await?;
2825        if gov_owncoins.is_empty() {
2826            return Err(Error::Custom(format!(
2827                "[dao_vote] Did not find any governance {} coins in wallet",
2828                dao.params.dao.gov_token_id
2829            )))
2830        }
2831
2832        // Find which governance coins we can use
2833        let gov_owncoins_to_use = match weight {
2834            Some(_weight) => {
2835                // TODO: Build a proper coin selection algorithm so that we can use a
2836                // coins combination that matches the requested weight
2837                return Err(Error::Custom(
2838                    "[dao_vote] Fractional vote weight not supported yet".to_string(),
2839                ))
2840            }
2841            // If no weight was specified, use them all
2842            None => gov_owncoins,
2843        };
2844
2845        // Now we need to do a lookup for the zkas proof bincodes, and create
2846        // the circuit objects and proving keys so we can build the transaction.
2847        // We also do this through the RPC. First we grab the fee call from money.
2848        let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
2849
2850        let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1)
2851        else {
2852            return Err(Error::Custom("[dao_vote] Fee circuit not found".to_string()))
2853        };
2854
2855        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
2856
2857        let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
2858
2859        // Creating Fee circuit proving key
2860        let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
2861
2862        // Now we grab the DAO bins
2863        let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?;
2864
2865        let Some(dao_vote_burn_zkbin) =
2866            zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_VOTE_INPUT_NS)
2867        else {
2868            return Err(Error::Custom("[dao_vote] DAO Vote Burn circuit not found".to_string()))
2869        };
2870
2871        let Some(dao_vote_main_zkbin) =
2872            zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_VOTE_MAIN_NS)
2873        else {
2874            return Err(Error::Custom("[dao_vote] DAO Vote Main circuit not found".to_string()))
2875        };
2876
2877        let dao_vote_burn_zkbin = ZkBinary::decode(&dao_vote_burn_zkbin.1)?;
2878        let dao_vote_main_zkbin = ZkBinary::decode(&dao_vote_main_zkbin.1)?;
2879
2880        let dao_vote_burn_circuit =
2881            ZkCircuit::new(empty_witnesses(&dao_vote_burn_zkbin)?, &dao_vote_burn_zkbin);
2882        let dao_vote_main_circuit =
2883            ZkCircuit::new(empty_witnesses(&dao_vote_main_zkbin)?, &dao_vote_main_zkbin);
2884
2885        // Creating DAO VoteBurn and VoteMain circuits proving keys
2886        let dao_vote_burn_pk = ProvingKey::build(dao_vote_burn_zkbin.k, &dao_vote_burn_circuit);
2887        let dao_vote_main_pk = ProvingKey::build(dao_vote_main_zkbin.k, &dao_vote_main_circuit);
2888
2889        // Now create the parameters for the vote tx
2890        let mut inputs = Vec::with_capacity(gov_owncoins_to_use.len());
2891        for gov_owncoin in gov_owncoins_to_use {
2892            // Skip governance coins that are not part of the snapshot
2893            let Ok(merkle_path) = proposal
2894                .money_snapshot_tree
2895                .as_ref()
2896                .unwrap()
2897                .witness(gov_owncoin.leaf_position, 0)
2898            else {
2899                continue
2900            };
2901            let nullifier = poseidon_hash([gov_owncoin.secret.inner(), gov_owncoin.coin.inner()]);
2902            let vote_nullifier =
2903                poseidon_hash([nullifier, gov_owncoin.secret.inner(), proposal_bulla.inner()]);
2904            if votes_nullifiers.contains(&vote_nullifier.into()) {
2905                return Err(Error::Custom("[dao_vote] Duplicate input nullifier found".to_string()))
2906            };
2907
2908            let input = DaoVoteInput {
2909                secret: gov_owncoin.secret,
2910                note: gov_owncoin.note.clone(),
2911                leaf_position: gov_owncoin.leaf_position,
2912                merkle_path,
2913            };
2914            inputs.push(input);
2915        }
2916        if inputs.is_empty() {
2917            return Err(Error::Custom(format!(
2918                "[dao_vote] Did not find any governance {} coins in wallet before proposal snapshot",
2919                dao.params.dao.gov_token_id
2920            )))
2921        }
2922
2923        // Retrieve next block height and current block time target,
2924        // to compute their window.
2925        let next_block_height = self.get_next_block_height().await?;
2926        let block_target = self.get_block_target().await?;
2927        let current_blockwindow = blockwindow(next_block_height, block_target);
2928
2929        // Generate the Money nullifiers Sparse Merkle Tree
2930        let store = MemoryStorageFp { tree: proposal.nullifiers_smt_snapshot.unwrap() };
2931        let money_null_smt = SmtMemoryFp::new(store, PoseidonFp::new(), &EMPTY_NODES_FP);
2932
2933        // Create the vote call
2934        let call = DaoVoteCall {
2935            money_null_smt: &money_null_smt,
2936            inputs,
2937            vote_option,
2938            proposal: proposal.proposal.clone(),
2939            dao: dao.params.dao.clone(),
2940            current_blockwindow,
2941        };
2942
2943        let (params, proofs, signature_secrets) = call.make(
2944            &dao_vote_burn_zkbin,
2945            &dao_vote_burn_pk,
2946            &dao_vote_main_zkbin,
2947            &dao_vote_main_pk,
2948        )?;
2949
2950        // Encode the call
2951        let mut data = vec![DaoFunction::Vote as u8];
2952        params.encode_async(&mut data).await?;
2953        let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
2954
2955        // Create the TransactionBuilder containing above call
2956        let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?;
2957
2958        // We first have to execute the fee-less tx to gather its used gas, and then we feed
2959        // it into the fee-creating function.
2960        let mut tx = tx_builder.build()?;
2961        let sigs = tx.create_sigs(&signature_secrets)?;
2962        tx.signatures.push(sigs);
2963
2964        let tree = self.get_money_tree().await?;
2965        let (fee_call, fee_proofs, fee_secrets) =
2966            self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?;
2967
2968        // Append the fee call to the transaction
2969        tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
2970
2971        // Now build the actual transaction and sign it with all necessary keys.
2972        let mut tx = tx_builder.build()?;
2973        let sigs = tx.create_sigs(&signature_secrets)?;
2974        tx.signatures.push(sigs);
2975        let sigs = tx.create_sigs(&fee_secrets)?;
2976        tx.signatures.push(sigs);
2977
2978        Ok(tx)
2979    }
2980
2981    /// Execute a DAO transfer proposal.
2982    pub async fn dao_exec_transfer(
2983        &self,
2984        proposal: &ProposalRecord,
2985        early: bool,
2986    ) -> Result<Transaction> {
2987        if proposal.leaf_position.is_none() ||
2988            proposal.money_snapshot_tree.is_none() ||
2989            proposal.nullifiers_smt_snapshot.is_none() ||
2990            proposal.tx_hash.is_none() ||
2991            proposal.call_index.is_none()
2992        {
2993            return Err(Error::Custom(
2994                "[dao_exec_transfer] Proposal seems to not have been deployed yet".to_string(),
2995            ))
2996        }
2997
2998        // Check proposal is not executed
2999        if let Some(exec_tx_hash) = proposal.exec_tx_hash {
3000            return Err(Error::Custom(format!(
3001                "[dao_exec_transfer] Proposal was executed on transaction: {exec_tx_hash}"
3002            )))
3003        }
3004
3005        // Check we know the plaintext data and they are valid
3006        if proposal.data.is_none() {
3007            return Err(Error::Custom(
3008                "[dao_exec_transfer] Proposal plainext data is empty".to_string(),
3009            ))
3010        }
3011        let proposal_coinattrs: CoinAttributes =
3012            deserialize_async(proposal.data.as_ref().unwrap()).await?;
3013
3014        // Fetch DAO and check its deployed
3015        let Ok(dao) = self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await else {
3016            return Err(Error::Custom(format!(
3017                "[dao_exec_transfer] DAO {} was not found",
3018                proposal.proposal.dao_bulla
3019            )))
3020        };
3021        if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() {
3022            return Err(Error::Custom(
3023                "[dao_exec_transfer] DAO seems to not have been deployed yet".to_string(),
3024            ))
3025        }
3026
3027        // Check that we have the exec key
3028        if dao.params.exec_secret_key.is_none() {
3029            return Err(Error::Custom(
3030                "[dao_exec_transfer] We need the exec secret key to execute proposals for this DAO"
3031                    .to_string(),
3032            ))
3033        }
3034
3035        // If early flag is provided, check that we have the early exec key
3036        if early && dao.params.early_exec_secret_key.is_none() {
3037            return Err(Error::Custom(
3038                "[dao_exec_transfer] We need the early exec secret key to execute proposals early for this DAO"
3039                    .to_string(),
3040            ))
3041        }
3042
3043        // Check proposal is approved
3044        let votes = self.get_dao_proposal_votes(&proposal.bulla()).await?;
3045        let mut yes_vote_value = 0;
3046        let mut yes_vote_blind = Blind::ZERO;
3047        let mut all_vote_value = 0;
3048        let mut all_vote_blind = Blind::ZERO;
3049        for vote in votes {
3050            if vote.vote_option {
3051                yes_vote_value += vote.all_vote_value;
3052            };
3053            yes_vote_blind += vote.yes_vote_blind;
3054            all_vote_value += vote.all_vote_value;
3055            all_vote_blind += vote.all_vote_blind;
3056        }
3057        let approval_ratio = (yes_vote_value as f64 * 100.0) / all_vote_value as f64;
3058        if all_vote_value < dao.params.dao.quorum ||
3059            approval_ratio <
3060                (dao.params.dao.approval_ratio_quot / dao.params.dao.approval_ratio_base)
3061                    as f64
3062        {
3063            return Err(Error::Custom(
3064                "[dao_exec_transfer] Proposal is not approved yet".to_string(),
3065            ))
3066        };
3067
3068        // Fetch DAO unspent OwnCoins to see what its balance is for the coin
3069        let dao_spend_hook =
3070            FuncRef { contract_id: *DAO_CONTRACT_ID, func_code: DaoFunction::Exec as u8 }
3071                .to_func_id();
3072        let dao_owncoins = self
3073            .get_contract_token_coins(
3074                &proposal_coinattrs.token_id,
3075                &dao_spend_hook,
3076                &proposal.proposal.dao_bulla.inner(),
3077            )
3078            .await?;
3079        if dao_owncoins.is_empty() {
3080            return Err(Error::Custom(format!(
3081                "[dao_exec_transfer] Did not find any {} unspent coins owned by this DAO",
3082                proposal_coinattrs.token_id,
3083            )))
3084        }
3085
3086        // Check DAO balance is sufficient
3087        if dao_owncoins.iter().map(|x| x.note.value).sum::<u64>() < proposal_coinattrs.value {
3088            return Err(Error::Custom(format!(
3089                "[dao_exec_transfer] Not enough DAO balance for token ID: {}",
3090                proposal_coinattrs.token_id,
3091            )))
3092        }
3093
3094        // Find which DAO coins we can use
3095        let (spent_coins, change_value) = select_coins(dao_owncoins, proposal_coinattrs.value)?;
3096
3097        // Now we need to do a lookup for the zkas proof bincodes, and create
3098        // the circuit objects and proving keys so we can build the transaction.
3099        // We also do this through the RPC. First we grab the calls from money.
3100        let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
3101
3102        let Some(mint_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_MINT_NS_V1)
3103        else {
3104            return Err(Error::Custom("[dao_exec_transfer] Mint circuit not found".to_string()))
3105        };
3106
3107        let Some(burn_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_BURN_NS_V1)
3108        else {
3109            return Err(Error::Custom("[dao_exec_transfer] Burn circuit not found".to_string()))
3110        };
3111
3112        let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1)
3113        else {
3114            return Err(Error::Custom("[dao_exec_transfer] Fee circuit not found".to_string()))
3115        };
3116
3117        let mint_zkbin = ZkBinary::decode(&mint_zkbin.1)?;
3118        let burn_zkbin = ZkBinary::decode(&burn_zkbin.1)?;
3119        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
3120
3121        let mint_circuit = ZkCircuit::new(empty_witnesses(&mint_zkbin)?, &mint_zkbin);
3122        let burn_circuit = ZkCircuit::new(empty_witnesses(&burn_zkbin)?, &burn_zkbin);
3123        let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
3124
3125        // Creating Mint, Burn and Fee circuits proving keys
3126        let mint_pk = ProvingKey::build(mint_zkbin.k, &mint_circuit);
3127        let burn_pk = ProvingKey::build(burn_zkbin.k, &burn_circuit);
3128        let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
3129
3130        // Now we grab the DAO bins
3131        let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?;
3132
3133        let (namespace, early_exec_secret_key) = match early {
3134            true => (
3135                DAO_CONTRACT_ZKAS_DAO_EARLY_EXEC_NS,
3136                Some(dao.params.early_exec_secret_key.unwrap()),
3137            ),
3138            false => (DAO_CONTRACT_ZKAS_DAO_EXEC_NS, None),
3139        };
3140
3141        let Some(dao_exec_zkbin) = zkas_bins.iter().find(|x| x.0 == namespace) else {
3142            return Err(Error::Custom(format!(
3143                "[dao_exec_transfer] DAO {namespace} circuit not found"
3144            )))
3145        };
3146
3147        let Some(dao_auth_transfer_zkbin) =
3148            zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_AUTH_MONEY_TRANSFER_NS)
3149        else {
3150            return Err(Error::Custom(
3151                "[dao_exec_transfer] DAO AuthTransfer circuit not found".to_string(),
3152            ))
3153        };
3154
3155        let Some(dao_auth_transfer_enc_coin_zkbin) =
3156            zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_AUTH_MONEY_TRANSFER_ENC_COIN_NS)
3157        else {
3158            return Err(Error::Custom(
3159                "[dao_exec_transfer] DAO AuthTransferEncCoin circuit not found".to_string(),
3160            ))
3161        };
3162
3163        let dao_exec_zkbin = ZkBinary::decode(&dao_exec_zkbin.1)?;
3164        let dao_auth_transfer_zkbin = ZkBinary::decode(&dao_auth_transfer_zkbin.1)?;
3165        let dao_auth_transfer_enc_coin_zkbin =
3166            ZkBinary::decode(&dao_auth_transfer_enc_coin_zkbin.1)?;
3167
3168        let dao_exec_circuit = ZkCircuit::new(empty_witnesses(&dao_exec_zkbin)?, &dao_exec_zkbin);
3169        let dao_auth_transfer_circuit =
3170            ZkCircuit::new(empty_witnesses(&dao_auth_transfer_zkbin)?, &dao_auth_transfer_zkbin);
3171        let dao_auth_transfer_enc_coin_circuit = ZkCircuit::new(
3172            empty_witnesses(&dao_auth_transfer_enc_coin_zkbin)?,
3173            &dao_auth_transfer_enc_coin_zkbin,
3174        );
3175
3176        // Creating DAO Exec, AuthTransfer and AuthTransferEncCoin circuits proving keys
3177        let dao_exec_pk = ProvingKey::build(dao_exec_zkbin.k, &dao_exec_circuit);
3178        let dao_auth_transfer_pk =
3179            ProvingKey::build(dao_auth_transfer_zkbin.k, &dao_auth_transfer_circuit);
3180        let dao_auth_transfer_enc_coin_pk = ProvingKey::build(
3181            dao_auth_transfer_enc_coin_zkbin.k,
3182            &dao_auth_transfer_enc_coin_circuit,
3183        );
3184
3185        // Fetch our money Merkle tree
3186        let tree = self.get_money_tree().await?;
3187
3188        // Retrieve next block height and current block time target,
3189        // to compute their window.
3190        let next_block_height = self.get_next_block_height().await?;
3191        let block_target = self.get_block_target().await?;
3192        let current_blockwindow = blockwindow(next_block_height, block_target);
3193
3194        // Now we can create the transfer call parameters
3195        let input_user_data_blind = Blind::random(&mut OsRng);
3196        let mut inputs = vec![];
3197        for coin in &spent_coins {
3198            inputs.push(TransferCallInput {
3199                coin: coin.clone(),
3200                merkle_path: tree.witness(coin.leaf_position, 0).unwrap(),
3201                user_data_blind: input_user_data_blind,
3202            });
3203        }
3204
3205        let mut outputs = vec![];
3206        outputs.push(proposal_coinattrs.clone());
3207
3208        let dao_coin_attrs = CoinAttributes {
3209            public_key: dao.params.dao.notes_public_key,
3210            value: change_value,
3211            token_id: proposal_coinattrs.token_id,
3212            spend_hook: dao_spend_hook,
3213            user_data: proposal.proposal.dao_bulla.inner(),
3214            blind: Blind::random(&mut OsRng),
3215        };
3216        outputs.push(dao_coin_attrs.clone());
3217
3218        // Create the transfer call
3219        let transfer_builder = TransferCallBuilder {
3220            clear_inputs: vec![],
3221            inputs,
3222            outputs,
3223            mint_zkbin: mint_zkbin.clone(),
3224            mint_pk: mint_pk.clone(),
3225            burn_zkbin: burn_zkbin.clone(),
3226            burn_pk: burn_pk.clone(),
3227        };
3228        let (transfer_params, transfer_secrets) = transfer_builder.build()?;
3229
3230        // Encode the call
3231        let mut data = vec![MoneyFunction::TransferV1 as u8];
3232        transfer_params.encode_async(&mut data).await?;
3233        let transfer_call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
3234
3235        // Create the exec call
3236        let exec_signature_secret = SecretKey::random(&mut OsRng);
3237        let exec_builder = DaoExecCall {
3238            proposal: proposal.proposal.clone(),
3239            dao: dao.params.dao.clone(),
3240            yes_vote_value,
3241            all_vote_value,
3242            yes_vote_blind,
3243            all_vote_blind,
3244            signature_secret: exec_signature_secret,
3245            current_blockwindow,
3246        };
3247        let (exec_params, exec_proofs) = exec_builder.make(
3248            &dao.params.exec_secret_key.unwrap(),
3249            &early_exec_secret_key,
3250            &dao_exec_zkbin,
3251            &dao_exec_pk,
3252        )?;
3253
3254        // Encode the call
3255        let mut data = vec![DaoFunction::Exec as u8];
3256        exec_params.encode_async(&mut data).await?;
3257        let exec_call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
3258
3259        // Now we can create the auth call
3260        // Auth module
3261        let auth_transfer_builder = DaoAuthMoneyTransferCall {
3262            proposal: proposal.proposal.clone(),
3263            proposal_coinattrs: vec![proposal_coinattrs],
3264            dao: dao.params.dao.clone(),
3265            input_user_data_blind,
3266            dao_coin_attrs,
3267        };
3268        let (auth_transfer_params, auth_transfer_proofs) = auth_transfer_builder.make(
3269            &dao_auth_transfer_zkbin,
3270            &dao_auth_transfer_pk,
3271            &dao_auth_transfer_enc_coin_zkbin,
3272            &dao_auth_transfer_enc_coin_pk,
3273        )?;
3274
3275        // Encode the call
3276        let mut data = vec![DaoFunction::AuthMoneyTransfer as u8];
3277        auth_transfer_params.encode_async(&mut data).await?;
3278        let auth_transfer_call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
3279
3280        // Create the TransactionBuilder containing above calls
3281        let mut tx_builder = TransactionBuilder::new(
3282            ContractCallLeaf { call: exec_call, proofs: exec_proofs },
3283            vec![
3284                DarkTree::new(
3285                    ContractCallLeaf { call: auth_transfer_call, proofs: auth_transfer_proofs },
3286                    vec![],
3287                    None,
3288                    None,
3289                ),
3290                DarkTree::new(
3291                    ContractCallLeaf { call: transfer_call, proofs: transfer_secrets.proofs },
3292                    vec![],
3293                    None,
3294                    None,
3295                ),
3296            ],
3297        )?;
3298
3299        // We first have to execute the fee-less tx to gather its used gas, and then we feed
3300        // it into the fee-creating function.
3301        let mut tx = tx_builder.build()?;
3302        let auth_transfer_sigs = tx.create_sigs(&[])?;
3303        let transfer_sigs = tx.create_sigs(&transfer_secrets.signature_secrets)?;
3304        let exec_sigs = tx.create_sigs(&[exec_signature_secret])?;
3305        tx.signatures = vec![auth_transfer_sigs, transfer_sigs, exec_sigs];
3306
3307        let (fee_call, fee_proofs, fee_secrets) =
3308            self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?;
3309
3310        // Append the fee call to the transaction
3311        tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
3312
3313        // Now build the actual transaction and sign it with all necessary keys.
3314        let mut tx = tx_builder.build()?;
3315        let sigs = tx.create_sigs(&[])?;
3316        tx.signatures.push(sigs);
3317        let sigs = tx.create_sigs(&transfer_secrets.signature_secrets)?;
3318        tx.signatures.push(sigs);
3319        let sigs = tx.create_sigs(&[exec_signature_secret])?;
3320        tx.signatures.push(sigs);
3321        let sigs = tx.create_sigs(&fee_secrets)?;
3322        tx.signatures.push(sigs);
3323
3324        Ok(tx)
3325    }
3326
3327    /// Execute a DAO generic proposal.
3328    pub async fn dao_exec_generic(
3329        &self,
3330        proposal: &ProposalRecord,
3331        early: bool,
3332    ) -> Result<Transaction> {
3333        if proposal.leaf_position.is_none() ||
3334            proposal.money_snapshot_tree.is_none() ||
3335            proposal.nullifiers_smt_snapshot.is_none() ||
3336            proposal.tx_hash.is_none() ||
3337            proposal.call_index.is_none()
3338        {
3339            return Err(Error::Custom(
3340                "[dao_exec_generic] Proposal seems to not have been deployed yet".to_string(),
3341            ))
3342        }
3343
3344        // Check proposal is not executed
3345        if let Some(exec_tx_hash) = proposal.exec_tx_hash {
3346            return Err(Error::Custom(format!(
3347                "[dao_exec_generic] Proposal was executed on transaction: {exec_tx_hash}"
3348            )))
3349        }
3350
3351        // Fetch DAO and check its deployed
3352        let Ok(dao) = self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await else {
3353            return Err(Error::Custom(format!(
3354                "[dao_exec_generic] DAO {} was not found",
3355                proposal.proposal.dao_bulla
3356            )))
3357        };
3358        if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() {
3359            return Err(Error::Custom(
3360                "[dao_exec_generic] DAO seems to not have been deployed yet".to_string(),
3361            ))
3362        }
3363
3364        // Check that we have the exec key
3365        if dao.params.exec_secret_key.is_none() {
3366            return Err(Error::Custom(
3367                "[dao_exec_generic] We need the exec secret key to execute proposals for this DAO"
3368                    .to_string(),
3369            ))
3370        }
3371
3372        // If early flag is provided, check that we have the early exec key
3373        if early && dao.params.early_exec_secret_key.is_none() {
3374            return Err(Error::Custom(
3375                "[dao_exec_generic] We need the early exec secret key to execute proposals early for this DAO"
3376                    .to_string(),
3377            ))
3378        }
3379
3380        // Check proposal is approved
3381        let votes = self.get_dao_proposal_votes(&proposal.bulla()).await?;
3382        let mut yes_vote_value = 0;
3383        let mut yes_vote_blind = Blind::ZERO;
3384        let mut all_vote_value = 0;
3385        let mut all_vote_blind = Blind::ZERO;
3386        for vote in votes {
3387            if vote.vote_option {
3388                yes_vote_value += vote.all_vote_value;
3389            };
3390            yes_vote_blind += vote.yes_vote_blind;
3391            all_vote_value += vote.all_vote_value;
3392            all_vote_blind += vote.all_vote_blind;
3393        }
3394        let approval_ratio = (yes_vote_value as f64 * 100.0) / all_vote_value as f64;
3395        if all_vote_value < dao.params.dao.quorum ||
3396            approval_ratio <
3397                (dao.params.dao.approval_ratio_quot / dao.params.dao.approval_ratio_base)
3398                    as f64
3399        {
3400            return Err(Error::Custom("[dao_exec_generic] Proposal is not approved yet".to_string()))
3401        };
3402
3403        // Now we need to do a lookup for the zkas proof bincodes, and create
3404        // the circuit objects and proving keys so we can build the transaction.
3405        // We also do this through the RPC. First we grab the calls from money.
3406        let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
3407        let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1)
3408        else {
3409            return Err(Error::Custom("[dao_exec_generic] Fee circuit not found".to_string()))
3410        };
3411        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
3412        let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
3413        let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
3414
3415        // Now we grab the DAO bins
3416        let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?;
3417
3418        let (namespace, early_exec_secret_key) = match early {
3419            true => (
3420                DAO_CONTRACT_ZKAS_DAO_EARLY_EXEC_NS,
3421                Some(dao.params.early_exec_secret_key.unwrap()),
3422            ),
3423            false => (DAO_CONTRACT_ZKAS_DAO_EXEC_NS, None),
3424        };
3425
3426        let Some(dao_exec_zkbin) = zkas_bins.iter().find(|x| x.0 == namespace) else {
3427            return Err(Error::Custom(format!(
3428                "[dao_exec_generic] DAO {namespace} circuit not found"
3429            )))
3430        };
3431        let dao_exec_zkbin = ZkBinary::decode(&dao_exec_zkbin.1)?;
3432        let dao_exec_circuit = ZkCircuit::new(empty_witnesses(&dao_exec_zkbin)?, &dao_exec_zkbin);
3433        let dao_exec_pk = ProvingKey::build(dao_exec_zkbin.k, &dao_exec_circuit);
3434
3435        // Fetch our money Merkle tree
3436        let tree = self.get_money_tree().await?;
3437
3438        // Retrieve next block height and current block time target,
3439        // to compute their window.
3440        let next_block_height = self.get_next_block_height().await?;
3441        let block_target = self.get_block_target().await?;
3442        let current_blockwindow = blockwindow(next_block_height, block_target);
3443
3444        // Create the exec call
3445        let exec_signature_secret = SecretKey::random(&mut OsRng);
3446        let exec_builder = DaoExecCall {
3447            proposal: proposal.proposal.clone(),
3448            dao: dao.params.dao.clone(),
3449            yes_vote_value,
3450            all_vote_value,
3451            yes_vote_blind,
3452            all_vote_blind,
3453            signature_secret: exec_signature_secret,
3454            current_blockwindow,
3455        };
3456        let (exec_params, exec_proofs) = exec_builder.make(
3457            &dao.params.exec_secret_key.unwrap(),
3458            &early_exec_secret_key,
3459            &dao_exec_zkbin,
3460            &dao_exec_pk,
3461        )?;
3462
3463        // Encode the call
3464        let mut data = vec![DaoFunction::Exec as u8];
3465        exec_params.encode_async(&mut data).await?;
3466        let exec_call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
3467
3468        // Create the TransactionBuilder containing above calls
3469        let mut tx_builder = TransactionBuilder::new(
3470            ContractCallLeaf { call: exec_call, proofs: exec_proofs },
3471            vec![],
3472        )?;
3473
3474        // We first have to execute the fee-less tx to gather its used gas, and then we feed
3475        // it into the fee-creating function.
3476        let mut tx = tx_builder.build()?;
3477        let exec_sigs = tx.create_sigs(&[exec_signature_secret])?;
3478        tx.signatures = vec![exec_sigs];
3479
3480        let (fee_call, fee_proofs, fee_secrets) =
3481            self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?;
3482
3483        // Append the fee call to the transaction
3484        tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
3485
3486        // Now build the actual transaction and sign it with all necessary keys.
3487        let mut tx = tx_builder.build()?;
3488        let sigs = tx.create_sigs(&[exec_signature_secret])?;
3489        tx.signatures.push(sigs);
3490        let sigs = tx.create_sigs(&fee_secrets)?;
3491        tx.signatures.push(sigs);
3492
3493        Ok(tx)
3494    }
3495}