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