drk/
dao.rs

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