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        if let Ok(dao) = self.get_dao_by_bulla(&bulla).await {
1790            output.push(format!("Updating \"{}\" DAO keys and name into the wallet", dao.name));
1791            let query = format!(
1792                "UPDATE {} SET {} = ?1, {} = ?2 WHERE {} = ?3;",
1793                *DAO_DAOS_TABLE, DAO_DAOS_COL_NAME, DAO_DAOS_COL_PARAMS, DAO_DAOS_COL_BULLA
1794            );
1795            if let Err(e) = self.wallet.exec_sql(
1796                &query,
1797                rusqlite::params![
1798                    name,
1799                    serialize_async(params).await,
1800                    serialize_async(&bulla).await
1801                ],
1802            ) {
1803                return Err(Error::DatabaseError(format!("[import_dao] DAO update failed: {e}")))
1804            };
1805            return Ok(())
1806        }
1807
1808        // Import the new DAO
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        Ok(())
1826    }
1827
1828    /// Remove a DAO and all its records given its name.
1829    pub async fn remove_dao(&self, name: &str, output: &mut Vec<String>) -> Result<()> {
1830        output.push(format!("Removing \"{name}\" DAO from the wallet"));
1831        let query = format!("DELETE FROM {} WHERE {} = ?1;", *DAO_DAOS_TABLE, DAO_DAOS_COL_NAME);
1832        if let Err(e) = self.wallet.exec_sql(&query, rusqlite::params![name]) {
1833            return Err(Error::DatabaseError(format!("[remove_dao] DAO removal failed: {e}")))
1834        };
1835        output.push(String::from("Successfully removed DAO"));
1836
1837        Ok(())
1838    }
1839
1840    /// Fetch a DAO given its bulla.
1841    pub async fn get_dao_by_bulla(&self, bulla: &DaoBulla) -> Result<DaoRecord> {
1842        let row = match self.wallet.query_single(
1843            &DAO_DAOS_TABLE,
1844            &[],
1845            convert_named_params! {(DAO_DAOS_COL_BULLA, serialize_async(bulla).await)},
1846        ) {
1847            Ok(r) => r,
1848            Err(e) => {
1849                return Err(Error::DatabaseError(format!(
1850                    "[get_dao_by_bulla] DAO retrieval failed: {e}"
1851                )))
1852            }
1853        };
1854
1855        self.parse_dao_record(&row).await
1856    }
1857
1858    /// Fetch a DAO given its name.
1859    pub async fn get_dao_by_name(&self, name: &str) -> Result<DaoRecord> {
1860        let row = match self.wallet.query_single(
1861            &DAO_DAOS_TABLE,
1862            &[],
1863            convert_named_params! {(DAO_DAOS_COL_NAME, name)},
1864        ) {
1865            Ok(r) => r,
1866            Err(e) => {
1867                return Err(Error::DatabaseError(format!(
1868                    "[get_dao_by_name] DAO retrieval failed: {e}"
1869                )))
1870            }
1871        };
1872
1873        self.parse_dao_record(&row).await
1874    }
1875
1876    /// List DAO(s) imported in the wallet. If a name is given, just print the
1877    /// metadata for that specific one, if found.
1878    pub async fn dao_list(&self, name: &Option<String>, output: &mut Vec<String>) -> Result<()> {
1879        if let Some(name) = name {
1880            let dao = self.get_dao_by_name(name).await?;
1881            output.push(format!("{dao}"));
1882            let address: Address =
1883                StandardAddress::from_public(self.network, dao.params.dao.notes_public_key).into();
1884            output.push(format!("Wallet Address: {address}"));
1885            return Ok(());
1886        }
1887
1888        let daos = self.get_daos().await?;
1889        for (i, dao) in daos.iter().enumerate() {
1890            output.push(format!("{i}. {}", dao.name));
1891        }
1892
1893        Ok(())
1894    }
1895
1896    /// Fetch known unspent balances from the wallet for the given DAO name.
1897    pub async fn dao_balance(&self, name: &str) -> Result<HashMap<String, u64>> {
1898        let dao = self.get_dao_by_name(name).await?;
1899
1900        let dao_spend_hook =
1901            FuncRef { contract_id: *DAO_CONTRACT_ID, func_code: DaoFunction::Exec as u8 }
1902                .to_func_id();
1903
1904        let mut coins = self.get_coins(false).await?;
1905        coins.retain(|x| x.0.note.spend_hook == dao_spend_hook);
1906        coins.retain(|x| x.0.note.user_data == dao.bulla().inner());
1907
1908        // Fill this map with balances
1909        let mut balmap: HashMap<String, u64> = HashMap::new();
1910
1911        for coin in coins {
1912            let mut value = coin.0.note.value;
1913
1914            if let Some(prev) = balmap.get(&coin.0.note.token_id.to_string()) {
1915                value += prev;
1916            }
1917
1918            balmap.insert(coin.0.note.token_id.to_string(), value);
1919        }
1920
1921        Ok(balmap)
1922    }
1923
1924    /// Fetch known unspent balances from the wallet for the given DAO name.
1925    pub async fn dao_mining_config(&self, name: &str, output: &mut Vec<String>) -> Result<()> {
1926        let dao = self.get_dao_by_name(name).await?;
1927        let address: Address =
1928            StandardAddress::from_public(self.network, dao.params.dao.notes_public_key).into();
1929        let recipient = address.to_string();
1930        let spend_hook = format!(
1931            "{}",
1932            FuncRef { contract_id: *DAO_CONTRACT_ID, func_code: DaoFunction::Exec as u8 }
1933                .to_func_id()
1934        );
1935        let user_data = bs58::encode(dao.bulla().inner().to_repr()).into_string();
1936        output.push(String::from("DarkFi DAO mining configuration address:"));
1937        output.push(
1938            base64::encode(&serialize(&(recipient, Some(spend_hook), Some(user_data)))).to_string(),
1939        );
1940
1941        Ok(())
1942    }
1943
1944    /// Fetch all known DAO proposalss from the wallet.
1945    pub async fn get_proposals(&self) -> Result<Vec<ProposalRecord>> {
1946        let rows = match self.wallet.query_multiple(&DAO_PROPOSALS_TABLE, &[], &[]) {
1947            Ok(r) => r,
1948            Err(e) => {
1949                return Err(Error::DatabaseError(format!(
1950                    "[get_proposals] DAO proposalss retrieval failed: {e}"
1951                )))
1952            }
1953        };
1954
1955        let mut daos = Vec::with_capacity(rows.len());
1956        for row in rows {
1957            daos.push(self.parse_dao_proposal(&row).await?);
1958        }
1959
1960        Ok(daos)
1961    }
1962
1963    /// Fetch a DAO proposal by its bulla.
1964    pub async fn get_dao_proposal_by_bulla(
1965        &self,
1966        bulla: &DaoProposalBulla,
1967    ) -> Result<ProposalRecord> {
1968        // Grab the proposal record
1969        let row = match self.wallet.query_single(
1970            &DAO_PROPOSALS_TABLE,
1971            &[],
1972            convert_named_params! {(DAO_PROPOSALS_COL_BULLA, serialize_async(bulla).await)},
1973        ) {
1974            Ok(r) => r,
1975            Err(e) => {
1976                return Err(Error::DatabaseError(format!(
1977                    "[get_dao_proposal_by_bulla] DAO proposal retrieval failed: {e}"
1978                )))
1979            }
1980        };
1981
1982        // Parse rest of the record
1983        self.parse_dao_proposal(&row).await
1984    }
1985
1986    // Fetch all known DAO proposal votes from the wallet given a proposal ID.
1987    pub async fn get_dao_proposal_votes(
1988        &self,
1989        proposal: &DaoProposalBulla,
1990    ) -> Result<Vec<VoteRecord>> {
1991        let rows = match self.wallet.query_multiple(
1992            &DAO_VOTES_TABLE,
1993            &[],
1994            convert_named_params! {(DAO_VOTES_COL_PROPOSAL_BULLA, serialize_async(proposal).await)},
1995        ) {
1996            Ok(r) => r,
1997            Err(e) => {
1998                return Err(Error::DatabaseError(format!(
1999                    "[get_dao_proposal_votes] Votes retrieval failed: {e}"
2000                )))
2001            }
2002        };
2003
2004        let mut votes = Vec::with_capacity(rows.len());
2005        for row in rows {
2006            let Value::Integer(id) = row[0] else {
2007                return Err(Error::ParseFailed("[get_dao_proposal_votes] ID parsing failed"))
2008            };
2009            let Ok(id) = u64::try_from(id) else {
2010                return Err(Error::ParseFailed("[get_dao_proposal_votes] ID parsing failed"))
2011            };
2012
2013            let Value::Blob(ref proposal_bytes) = row[1] else {
2014                return Err(Error::ParseFailed(
2015                    "[get_dao_proposal_votes] Proposal bytes bytes parsing failed",
2016                ))
2017            };
2018            let proposal = deserialize_async(proposal_bytes).await?;
2019
2020            let Value::Integer(vote_option) = row[2] else {
2021                return Err(Error::ParseFailed(
2022                    "[get_dao_proposal_votes] Vote option parsing failed",
2023                ))
2024            };
2025            let Ok(vote_option) = u32::try_from(vote_option) else {
2026                return Err(Error::ParseFailed(
2027                    "[get_dao_proposal_votes] Vote option parsing failed",
2028                ))
2029            };
2030            let vote_option = vote_option != 0;
2031
2032            let Value::Blob(ref yes_vote_blind_bytes) = row[3] else {
2033                return Err(Error::ParseFailed(
2034                    "[get_dao_proposal_votes] Yes vote blind bytes parsing failed",
2035                ))
2036            };
2037            let yes_vote_blind = deserialize_async(yes_vote_blind_bytes).await?;
2038
2039            let Value::Blob(ref all_vote_value_bytes) = row[4] else {
2040                return Err(Error::ParseFailed(
2041                    "[get_dao_proposal_votes] All vote value bytes parsing failed",
2042                ))
2043            };
2044            let all_vote_value = deserialize_async(all_vote_value_bytes).await?;
2045
2046            let Value::Blob(ref all_vote_blind_bytes) = row[5] else {
2047                return Err(Error::ParseFailed(
2048                    "[get_dao_proposal_votes] All vote blind bytes parsing failed",
2049                ))
2050            };
2051            let all_vote_blind = deserialize_async(all_vote_blind_bytes).await?;
2052
2053            let Value::Integer(block_height) = row[6] else {
2054                return Err(Error::ParseFailed(
2055                    "[get_dao_proposal_votes] Block height parsing failed",
2056                ))
2057            };
2058            let Ok(block_height) = u32::try_from(block_height) else {
2059                return Err(Error::ParseFailed(
2060                    "[get_dao_proposal_votes] Block height parsing failed",
2061                ))
2062            };
2063
2064            let Value::Blob(ref tx_hash_bytes) = row[7] else {
2065                return Err(Error::ParseFailed(
2066                    "[get_dao_proposal_votes] Transaction hash bytes parsing failed",
2067                ))
2068            };
2069            let tx_hash = deserialize_async(tx_hash_bytes).await?;
2070
2071            let Value::Integer(call_index) = row[8] else {
2072                return Err(Error::ParseFailed("[get_dao_proposal_votes] Call index parsing failed"))
2073            };
2074            let Ok(call_index) = u8::try_from(call_index) else {
2075                return Err(Error::ParseFailed("[get_dao_proposal_votes] Call index parsing failed"))
2076            };
2077
2078            let Value::Blob(ref nullifiers_bytes) = row[9] else {
2079                return Err(Error::ParseFailed(
2080                    "[get_dao_proposal_votes] Nullifiers bytes parsing failed",
2081                ))
2082            };
2083            let nullifiers = deserialize_async(nullifiers_bytes).await?;
2084
2085            let vote = VoteRecord {
2086                id,
2087                proposal,
2088                vote_option,
2089                yes_vote_blind,
2090                all_vote_value,
2091                all_vote_blind,
2092                block_height,
2093                tx_hash,
2094                call_index,
2095                nullifiers,
2096            };
2097
2098            votes.push(vote);
2099        }
2100
2101        Ok(votes)
2102    }
2103
2104    /// Mint a DAO on-chain.
2105    pub async fn dao_mint(&self, name: &str) -> Result<Transaction> {
2106        // Retrieve the dao record
2107        let dao = self.get_dao_by_name(name).await?;
2108
2109        // Check its not already minted
2110        if dao.tx_hash.is_some() {
2111            return Err(Error::Custom(
2112                "[dao_mint] This DAO seems to have already been minted on-chain".to_string(),
2113            ))
2114        }
2115
2116        // Check that we have all the keys
2117        if dao.params.notes_secret_key.is_none() ||
2118            dao.params.proposer_secret_key.is_none() ||
2119            dao.params.proposals_secret_key.is_none() ||
2120            dao.params.votes_secret_key.is_none() ||
2121            dao.params.exec_secret_key.is_none() ||
2122            dao.params.early_exec_secret_key.is_none()
2123        {
2124            return Err(Error::Custom(
2125                "[dao_mint] We need all the secrets key to mint the DAO on-chain".to_string(),
2126            ))
2127        }
2128
2129        // Now we need to do a lookup for the zkas proof bincodes, and create
2130        // the circuit objects and proving keys so we can build the transaction.
2131        // We also do this through the RPC. First we grab the fee call from money.
2132        let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
2133
2134        let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1)
2135        else {
2136            return Err(Error::Custom("Fee circuit not found".to_string()))
2137        };
2138
2139        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1, false)?;
2140
2141        let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
2142
2143        // Creating Fee circuit proving key
2144        let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
2145
2146        // Now we grab the DAO mint
2147        let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?;
2148
2149        let Some(dao_mint_zkbin) = zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_MINT_NS)
2150        else {
2151            return Err(Error::DatabaseError("[dao_mint] DAO Mint circuit not found".to_string()))
2152        };
2153
2154        let dao_mint_zkbin = ZkBinary::decode(&dao_mint_zkbin.1, false)?;
2155
2156        let dao_mint_circuit = ZkCircuit::new(empty_witnesses(&dao_mint_zkbin)?, &dao_mint_zkbin);
2157
2158        // Creating DAO Mint circuit proving key
2159        let dao_mint_pk = ProvingKey::build(dao_mint_zkbin.k, &dao_mint_circuit);
2160
2161        // Create the DAO mint call
2162        let notes_secret_key = dao.params.notes_secret_key.unwrap();
2163        let (params, proofs) = make_mint_call(
2164            &dao.params.dao,
2165            &notes_secret_key,
2166            &dao.params.proposer_secret_key.unwrap(),
2167            &dao.params.proposals_secret_key.unwrap(),
2168            &dao.params.votes_secret_key.unwrap(),
2169            &dao.params.exec_secret_key.unwrap(),
2170            &dao.params.early_exec_secret_key.unwrap(),
2171            &dao_mint_zkbin,
2172            &dao_mint_pk,
2173        )?;
2174        let mut data = vec![DaoFunction::Mint as u8];
2175        params.encode_async(&mut data).await?;
2176        let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
2177
2178        // Create the TransactionBuilder containing above call
2179        let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?;
2180
2181        // We first have to execute the fee-less tx to gather its used gas, and then we feed
2182        // it into the fee-creating function.
2183        let mut tx = tx_builder.build()?;
2184        let sigs = tx.create_sigs(&[notes_secret_key])?;
2185        tx.signatures.push(sigs);
2186
2187        let tree = self.get_money_tree().await?;
2188        let (fee_call, fee_proofs, fee_secrets) =
2189            self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?;
2190
2191        // Append the fee call to the transaction
2192        tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
2193
2194        // Now build the actual transaction and sign it with all necessary keys.
2195        let mut tx = tx_builder.build()?;
2196        let sigs = tx.create_sigs(&[notes_secret_key])?;
2197        tx.signatures.push(sigs);
2198        let sigs = tx.create_sigs(&fee_secrets)?;
2199        tx.signatures.push(sigs);
2200
2201        Ok(tx)
2202    }
2203
2204    /// Create a DAO transfer proposal.
2205    #[allow(clippy::too_many_arguments)]
2206    pub async fn dao_propose_transfer(
2207        &self,
2208        name: &str,
2209        duration_blockwindows: u64,
2210        amount: &str,
2211        token_id: TokenId,
2212        recipient: PublicKey,
2213        spend_hook: Option<FuncId>,
2214        user_data: Option<pallas::Base>,
2215    ) -> Result<ProposalRecord> {
2216        // Fetch DAO and check its deployed
2217        let dao = self.get_dao_by_name(name).await?;
2218        if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() {
2219            return Err(Error::Custom(
2220                "[dao_propose_transfer] DAO seems to not have been deployed yet".to_string(),
2221            ))
2222        }
2223
2224        // Check that we have the proposer key
2225        if dao.params.proposer_secret_key.is_none() {
2226            return Err(Error::Custom(
2227                "[dao_propose_transfer] We need the proposer secret key to create proposals for this DAO".to_string(),
2228            ))
2229        }
2230
2231        // Fetch DAO unspent OwnCoins to see what its balance is
2232        let dao_spend_hook =
2233            FuncRef { contract_id: *DAO_CONTRACT_ID, func_code: DaoFunction::Exec as u8 }
2234                .to_func_id();
2235        let dao_bulla = dao.bulla();
2236        let dao_owncoins =
2237            self.get_contract_token_coins(&token_id, &dao_spend_hook, &dao_bulla.inner()).await?;
2238        if dao_owncoins.is_empty() {
2239            return Err(Error::Custom(format!(
2240                "[dao_propose_transfer] Did not find any {token_id} unspent coins owned by this DAO"
2241            )))
2242        }
2243
2244        // Check DAO balance is sufficient
2245        let amount = decode_base10(amount, BALANCE_BASE10_DECIMALS, false)?;
2246        if dao_owncoins.iter().map(|x| x.note.value).sum::<u64>() < amount {
2247            return Err(Error::Custom(format!(
2248                "[dao_propose_transfer] Not enough DAO balance for token ID: {token_id}",
2249            )))
2250        }
2251
2252        // Generate proposal coin attributes
2253        let proposal_coinattrs = CoinAttributes {
2254            public_key: recipient,
2255            value: amount,
2256            token_id,
2257            spend_hook: spend_hook.unwrap_or(FuncId::none()),
2258            user_data: user_data.unwrap_or(pallas::Base::ZERO),
2259            blind: Blind::random(&mut OsRng),
2260        };
2261
2262        // Convert coin_params to actual coins
2263        let proposal_coins = vec![proposal_coinattrs.to_coin()];
2264        let mut proposal_data = vec![];
2265        proposal_coins.encode_async(&mut proposal_data).await?;
2266
2267        // Create Auth calls
2268        let auth_calls = vec![
2269            DaoAuthCall {
2270                contract_id: *DAO_CONTRACT_ID,
2271                function_code: DaoFunction::AuthMoneyTransfer as u8,
2272                auth_data: proposal_data,
2273            },
2274            DaoAuthCall {
2275                contract_id: *MONEY_CONTRACT_ID,
2276                function_code: MoneyFunction::TransferV1 as u8,
2277                auth_data: vec![],
2278            },
2279        ];
2280
2281        // Retrieve next block height and current block time target,
2282        // to compute their window.
2283        let next_block_height = self.get_next_block_height().await?;
2284        let block_target = self.get_block_target().await?;
2285        let creation_blockwindow = blockwindow(next_block_height, block_target);
2286
2287        // Create the actual proposal
2288        let proposal = DaoProposal {
2289            auth_calls,
2290            creation_blockwindow,
2291            duration_blockwindows,
2292            user_data: user_data.unwrap_or(pallas::Base::ZERO),
2293            dao_bulla,
2294            blind: Blind::random(&mut OsRng),
2295        };
2296
2297        let proposal_record = ProposalRecord {
2298            proposal,
2299            data: Some(serialize_async(&proposal_coinattrs).await),
2300            leaf_position: None,
2301            money_snapshot_tree: None,
2302            nullifiers_smt_snapshot: None,
2303            mint_height: None,
2304            tx_hash: None,
2305            call_index: None,
2306            exec_height: None,
2307            exec_tx_hash: None,
2308        };
2309
2310        if let Err(e) = self.put_dao_proposal(&proposal_record).await {
2311            return Err(Error::DatabaseError(format!(
2312                "[dao_propose_transfer] Put DAO proposal failed: {e}"
2313            )))
2314        }
2315
2316        Ok(proposal_record)
2317    }
2318
2319    /// Create a DAO generic proposal.
2320    pub async fn dao_propose_generic(
2321        &self,
2322        name: &str,
2323        duration_blockwindows: u64,
2324        user_data: Option<pallas::Base>,
2325    ) -> Result<ProposalRecord> {
2326        // Fetch DAO and check its deployed
2327        let dao = self.get_dao_by_name(name).await?;
2328        if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() {
2329            return Err(Error::Custom(
2330                "[dao_propose_generic] DAO seems to not have been deployed yet".to_string(),
2331            ))
2332        }
2333
2334        // Check that we have the proposer key
2335        if dao.params.proposer_secret_key.is_none() {
2336            return Err(Error::Custom(
2337                "[dao_propose_generic] We need the proposer secret key to create proposals for this DAO".to_string(),
2338            ))
2339        }
2340
2341        // Retrieve next block height and current block time target,
2342        // to compute their window.
2343        let next_block_height = self.get_next_block_height().await?;
2344        let block_target = self.get_block_target().await?;
2345        let creation_blockwindow = blockwindow(next_block_height, block_target);
2346
2347        // Create the actual proposal
2348        let proposal = DaoProposal {
2349            auth_calls: vec![],
2350            creation_blockwindow,
2351            duration_blockwindows,
2352            user_data: user_data.unwrap_or(pallas::Base::ZERO),
2353            dao_bulla: dao.bulla(),
2354            blind: Blind::random(&mut OsRng),
2355        };
2356
2357        let proposal_record = ProposalRecord {
2358            proposal,
2359            data: None,
2360            leaf_position: None,
2361            money_snapshot_tree: None,
2362            nullifiers_smt_snapshot: None,
2363            mint_height: None,
2364            tx_hash: None,
2365            call_index: None,
2366            exec_height: None,
2367            exec_tx_hash: None,
2368        };
2369
2370        if let Err(e) = self.put_dao_proposal(&proposal_record).await {
2371            return Err(Error::DatabaseError(format!(
2372                "[dao_propose_generic] Put DAO proposal failed: {e}"
2373            )))
2374        }
2375
2376        Ok(proposal_record)
2377    }
2378
2379    /// Create a DAO transfer proposal transaction.
2380    pub async fn dao_transfer_proposal_tx(&self, proposal: &ProposalRecord) -> Result<Transaction> {
2381        // Check we know the plaintext data
2382        if proposal.data.is_none() {
2383            return Err(Error::Custom(
2384                "[dao_transfer_proposal_tx] Proposal plainext data is empty".to_string(),
2385            ))
2386        }
2387        let proposal_coinattrs: CoinAttributes =
2388            deserialize_async(proposal.data.as_ref().unwrap()).await?;
2389
2390        // Fetch DAO and check its deployed
2391        let Ok(dao) = self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await else {
2392            return Err(Error::Custom(format!(
2393                "[dao_transfer_proposal_tx] DAO {} was not found",
2394                proposal.proposal.dao_bulla
2395            )))
2396        };
2397        if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() {
2398            return Err(Error::Custom(
2399                "[dao_transfer_proposal_tx] DAO seems to not have been deployed yet".to_string(),
2400            ))
2401        }
2402
2403        // Check that we have the proposer key
2404        if dao.params.proposer_secret_key.is_none() {
2405            return Err(Error::Custom(
2406                "[dao_transfer_proposal_tx] We need the proposer secret key to create proposals for this DAO".to_string(),
2407            ))
2408        }
2409
2410        // Fetch DAO unspent OwnCoins to see what its balance is for the coin
2411        let dao_spend_hook =
2412            FuncRef { contract_id: *DAO_CONTRACT_ID, func_code: DaoFunction::Exec as u8 }
2413                .to_func_id();
2414        let dao_owncoins = self
2415            .get_contract_token_coins(
2416                &proposal_coinattrs.token_id,
2417                &dao_spend_hook,
2418                &proposal.proposal.dao_bulla.inner(),
2419            )
2420            .await?;
2421        if dao_owncoins.is_empty() {
2422            return Err(Error::Custom(format!(
2423                "[dao_transfer_proposal_tx] Did not find any {} unspent coins owned by this DAO",
2424                proposal_coinattrs.token_id,
2425            )))
2426        }
2427
2428        // Check DAO balance is sufficient
2429        if dao_owncoins.iter().map(|x| x.note.value).sum::<u64>() < proposal_coinattrs.value {
2430            return Err(Error::Custom(format!(
2431                "[dao_transfer_proposal_tx] Not enough DAO balance for token ID: {}",
2432                proposal_coinattrs.token_id,
2433            )))
2434        }
2435
2436        // Fetch our own governance OwnCoins to see what our balance is
2437        let gov_owncoins = self.get_token_coins(&dao.params.dao.gov_token_id).await?;
2438        if gov_owncoins.is_empty() {
2439            return Err(Error::Custom(format!(
2440                "[dao_transfer_proposal_tx] Did not find any governance {} coins in wallet",
2441                dao.params.dao.gov_token_id
2442            )))
2443        }
2444
2445        // Find which governance coins we can use
2446        let mut total_value = 0;
2447        let mut gov_owncoins_to_use = vec![];
2448        for gov_owncoin in gov_owncoins {
2449            if total_value >= dao.params.dao.proposer_limit {
2450                break
2451            }
2452
2453            total_value += gov_owncoin.note.value;
2454            gov_owncoins_to_use.push(gov_owncoin);
2455        }
2456
2457        // Check our governance coins balance is sufficient
2458        if total_value < dao.params.dao.proposer_limit {
2459            return Err(Error::Custom(format!(
2460                "[dao_transfer_proposal_tx] Not enough gov token {} balance to propose",
2461                dao.params.dao.gov_token_id
2462            )))
2463        }
2464
2465        // Now we need to do a lookup for the zkas proof bincodes, and create
2466        // the circuit objects and proving keys so we can build the transaction.
2467        // We also do this through the RPC. First we grab the fee call from money.
2468        let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
2469
2470        let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1)
2471        else {
2472            return Err(Error::Custom(
2473                "[dao_transfer_proposal_tx] Fee circuit not found".to_string(),
2474            ))
2475        };
2476
2477        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1, false)?;
2478
2479        let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
2480
2481        // Creating Fee circuit proving key
2482        let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
2483
2484        // Now we grab the DAO bins
2485        let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?;
2486
2487        let Some(propose_burn_zkbin) =
2488            zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_PROPOSE_INPUT_NS)
2489        else {
2490            return Err(Error::Custom(
2491                "[dao_transfer_proposal_tx] Propose Burn circuit not found".to_string(),
2492            ))
2493        };
2494
2495        let Some(propose_main_zkbin) =
2496            zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_PROPOSE_MAIN_NS)
2497        else {
2498            return Err(Error::Custom(
2499                "[dao_transfer_proposal_tx] Propose Main circuit not found".to_string(),
2500            ))
2501        };
2502
2503        let propose_burn_zkbin = ZkBinary::decode(&propose_burn_zkbin.1, false)?;
2504        let propose_main_zkbin = ZkBinary::decode(&propose_main_zkbin.1, false)?;
2505
2506        let propose_burn_circuit =
2507            ZkCircuit::new(empty_witnesses(&propose_burn_zkbin)?, &propose_burn_zkbin);
2508        let propose_main_circuit =
2509            ZkCircuit::new(empty_witnesses(&propose_main_zkbin)?, &propose_main_zkbin);
2510
2511        // Creating DAO ProposeBurn and ProposeMain circuits proving keys
2512        let propose_burn_pk = ProvingKey::build(propose_burn_zkbin.k, &propose_burn_circuit);
2513        let propose_main_pk = ProvingKey::build(propose_main_zkbin.k, &propose_main_circuit);
2514
2515        // Fetch our money Merkle tree
2516        let money_merkle_tree = self.get_money_tree().await?;
2517
2518        // Now we can create the proposal transaction parameters.
2519        // We first generate the `DaoProposeStakeInput` inputs,
2520        // using our governance OwnCoins.
2521        let mut inputs = Vec::with_capacity(gov_owncoins_to_use.len());
2522        for gov_owncoin in gov_owncoins_to_use {
2523            let input = DaoProposeStakeInput {
2524                secret: gov_owncoin.secret,
2525                note: gov_owncoin.note.clone(),
2526                leaf_position: gov_owncoin.leaf_position,
2527                merkle_path: money_merkle_tree.witness(gov_owncoin.leaf_position, 0).unwrap(),
2528            };
2529            inputs.push(input);
2530        }
2531
2532        // Now create the parameters for the proposal tx.
2533        // Fetch the daos Merkle tree to compute the DAO Merkle path and root.
2534        let (daos_tree, _) = self.get_dao_trees().await?;
2535        let (dao_merkle_path, dao_merkle_root) = {
2536            let root = daos_tree.root(0).unwrap();
2537            let leaf_pos = dao.leaf_position.unwrap();
2538            let dao_merkle_path = daos_tree.witness(leaf_pos, 0).unwrap();
2539            (dao_merkle_path, root)
2540        };
2541
2542        // Generate the Money nullifiers Sparse Merkle Tree
2543        let store = CacheSmtStorage::new(CacheOverlay::new(&self.cache)?, SLED_MONEY_SMT_TREE);
2544        let money_null_smt = CacheSmt::new(store, PoseidonFp::new(), &EMPTY_NODES_FP);
2545
2546        // Create the proposal call
2547        let call = DaoProposeCall {
2548            money_null_smt: &money_null_smt,
2549            inputs,
2550            proposal: proposal.proposal.clone(),
2551            dao: dao.params.dao,
2552            dao_leaf_position: dao.leaf_position.unwrap(),
2553            dao_merkle_path,
2554            dao_merkle_root,
2555        };
2556
2557        let (params, proofs, signature_secrets) = call.make(
2558            &dao.params.proposer_secret_key.unwrap(),
2559            &propose_burn_zkbin,
2560            &propose_burn_pk,
2561            &propose_main_zkbin,
2562            &propose_main_pk,
2563        )?;
2564
2565        // Encode the call
2566        let mut data = vec![DaoFunction::Propose as u8];
2567        params.encode_async(&mut data).await?;
2568        let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
2569
2570        // Create the TransactionBuilder containing above call
2571        let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?;
2572
2573        // We first have to execute the fee-less tx to gather its used gas, and then we feed
2574        // it into the fee-creating function.
2575        let mut tx = tx_builder.build()?;
2576        let sigs = tx.create_sigs(&signature_secrets)?;
2577        tx.signatures.push(sigs);
2578
2579        let tree = self.get_money_tree().await?;
2580        let (fee_call, fee_proofs, fee_secrets) =
2581            self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?;
2582
2583        // Append the fee call to the transaction
2584        tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
2585
2586        // Now build the actual transaction and sign it with all necessary keys.
2587        let mut tx = tx_builder.build()?;
2588        let sigs = tx.create_sigs(&signature_secrets)?;
2589        tx.signatures.push(sigs);
2590        let sigs = tx.create_sigs(&fee_secrets)?;
2591        tx.signatures.push(sigs);
2592
2593        Ok(tx)
2594    }
2595
2596    /// Create a DAO generic proposal transaction.
2597    pub async fn dao_generic_proposal_tx(&self, proposal: &ProposalRecord) -> Result<Transaction> {
2598        // Fetch DAO and check its deployed
2599        let Ok(dao) = self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await else {
2600            return Err(Error::Custom(format!(
2601                "[dao_generic_proposal_tx] DAO {} was not found",
2602                proposal.proposal.dao_bulla
2603            )))
2604        };
2605        if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() {
2606            return Err(Error::Custom(
2607                "[dao_generic_proposal_tx] DAO seems to not have been deployed yet".to_string(),
2608            ))
2609        }
2610
2611        // Check that we have the proposer key
2612        if dao.params.proposer_secret_key.is_none() {
2613            return Err(Error::Custom(
2614                "[dao_generic_proposal_tx] We need the proposer secret key to create proposals for this DAO".to_string(),
2615            ))
2616        }
2617
2618        // Fetch our own governance OwnCoins to see what our balance is
2619        let gov_owncoins = self.get_token_coins(&dao.params.dao.gov_token_id).await?;
2620        if gov_owncoins.is_empty() {
2621            return Err(Error::Custom(format!(
2622                "[dao_generic_proposal_tx] Did not find any governance {} coins in wallet",
2623                dao.params.dao.gov_token_id
2624            )))
2625        }
2626
2627        // Find which governance coins we can use
2628        let mut total_value = 0;
2629        let mut gov_owncoins_to_use = vec![];
2630        for gov_owncoin in gov_owncoins {
2631            if total_value >= dao.params.dao.proposer_limit {
2632                break
2633            }
2634
2635            total_value += gov_owncoin.note.value;
2636            gov_owncoins_to_use.push(gov_owncoin);
2637        }
2638
2639        // Check our governance coins balance is sufficient
2640        if total_value < dao.params.dao.proposer_limit {
2641            return Err(Error::Custom(format!(
2642                "[dao_generic_proposal_tx] Not enough gov token {} balance to propose",
2643                dao.params.dao.gov_token_id
2644            )))
2645        }
2646
2647        // Now we need to do a lookup for the zkas proof bincodes, and create
2648        // the circuit objects and proving keys so we can build the transaction.
2649        // We also do this through the RPC. First we grab the fee call from money.
2650        let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
2651
2652        let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1)
2653        else {
2654            return Err(Error::Custom("[dao_generic_proposal_tx] Fee circuit not found".to_string()))
2655        };
2656
2657        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1, false)?;
2658
2659        let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
2660
2661        // Creating Fee circuit proving key
2662        let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
2663
2664        // Now we grab the DAO bins
2665        let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?;
2666
2667        let Some(propose_burn_zkbin) =
2668            zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_PROPOSE_INPUT_NS)
2669        else {
2670            return Err(Error::Custom(
2671                "[dao_generic_proposal_tx] Propose Burn circuit not found".to_string(),
2672            ))
2673        };
2674
2675        let Some(propose_main_zkbin) =
2676            zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_PROPOSE_MAIN_NS)
2677        else {
2678            return Err(Error::Custom(
2679                "[dao_generic_proposal_tx] Propose Main circuit not found".to_string(),
2680            ))
2681        };
2682
2683        let propose_burn_zkbin = ZkBinary::decode(&propose_burn_zkbin.1, false)?;
2684        let propose_main_zkbin = ZkBinary::decode(&propose_main_zkbin.1, false)?;
2685
2686        let propose_burn_circuit =
2687            ZkCircuit::new(empty_witnesses(&propose_burn_zkbin)?, &propose_burn_zkbin);
2688        let propose_main_circuit =
2689            ZkCircuit::new(empty_witnesses(&propose_main_zkbin)?, &propose_main_zkbin);
2690
2691        // Creating DAO ProposeBurn and ProposeMain circuits proving keys
2692        let propose_burn_pk = ProvingKey::build(propose_burn_zkbin.k, &propose_burn_circuit);
2693        let propose_main_pk = ProvingKey::build(propose_main_zkbin.k, &propose_main_circuit);
2694
2695        // Fetch our money Merkle tree
2696        let money_merkle_tree = self.get_money_tree().await?;
2697
2698        // Now we can create the proposal transaction parameters.
2699        // We first generate the `DaoProposeStakeInput` inputs,
2700        // using our governance OwnCoins.
2701        let mut inputs = Vec::with_capacity(gov_owncoins_to_use.len());
2702        for gov_owncoin in gov_owncoins_to_use {
2703            let input = DaoProposeStakeInput {
2704                secret: gov_owncoin.secret,
2705                note: gov_owncoin.note.clone(),
2706                leaf_position: gov_owncoin.leaf_position,
2707                merkle_path: money_merkle_tree.witness(gov_owncoin.leaf_position, 0).unwrap(),
2708            };
2709            inputs.push(input);
2710        }
2711
2712        // Now create the parameters for the proposal tx.
2713        // Fetch the daos Merkle tree to compute the DAO Merkle path and root.
2714        let (daos_tree, _) = self.get_dao_trees().await?;
2715        let (dao_merkle_path, dao_merkle_root) = {
2716            let root = daos_tree.root(0).unwrap();
2717            let leaf_pos = dao.leaf_position.unwrap();
2718            let dao_merkle_path = daos_tree.witness(leaf_pos, 0).unwrap();
2719            (dao_merkle_path, root)
2720        };
2721
2722        // Generate the Money nullifiers Sparse Merkle Tree
2723        let store = CacheSmtStorage::new(CacheOverlay::new(&self.cache)?, SLED_MONEY_SMT_TREE);
2724        let money_null_smt = CacheSmt::new(store, PoseidonFp::new(), &EMPTY_NODES_FP);
2725
2726        // Create the proposal call
2727        let call = DaoProposeCall {
2728            money_null_smt: &money_null_smt,
2729            inputs,
2730            proposal: proposal.proposal.clone(),
2731            dao: dao.params.dao,
2732            dao_leaf_position: dao.leaf_position.unwrap(),
2733            dao_merkle_path,
2734            dao_merkle_root,
2735        };
2736
2737        let (params, proofs, signature_secrets) = call.make(
2738            &dao.params.proposer_secret_key.unwrap(),
2739            &propose_burn_zkbin,
2740            &propose_burn_pk,
2741            &propose_main_zkbin,
2742            &propose_main_pk,
2743        )?;
2744
2745        // Encode the call
2746        let mut data = vec![DaoFunction::Propose as u8];
2747        params.encode_async(&mut data).await?;
2748        let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
2749
2750        // Create the TransactionBuilder containing above call
2751        let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?;
2752
2753        // We first have to execute the fee-less tx to gather its used gas, and then we feed
2754        // it into the fee-creating function.
2755        let mut tx = tx_builder.build()?;
2756        let sigs = tx.create_sigs(&signature_secrets)?;
2757        tx.signatures.push(sigs);
2758
2759        let tree = self.get_money_tree().await?;
2760        let (fee_call, fee_proofs, fee_secrets) =
2761            self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?;
2762
2763        // Append the fee call to the transaction
2764        tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
2765
2766        // Now build the actual transaction and sign it with all necessary keys.
2767        let mut tx = tx_builder.build()?;
2768        let sigs = tx.create_sigs(&signature_secrets)?;
2769        tx.signatures.push(sigs);
2770        let sigs = tx.create_sigs(&fee_secrets)?;
2771        tx.signatures.push(sigs);
2772
2773        Ok(tx)
2774    }
2775
2776    /// Vote on a DAO proposal
2777    pub async fn dao_vote(
2778        &self,
2779        proposal_bulla: &DaoProposalBulla,
2780        vote_option: bool,
2781        weight: Option<u64>,
2782    ) -> Result<Transaction> {
2783        // Feth the proposal and check its deployed
2784        let Ok(proposal) = self.get_dao_proposal_by_bulla(proposal_bulla).await else {
2785            return Err(Error::Custom(format!("[dao_vote] Proposal {proposal_bulla} was not found")))
2786        };
2787        if proposal.leaf_position.is_none() ||
2788            proposal.money_snapshot_tree.is_none() ||
2789            proposal.nullifiers_smt_snapshot.is_none() ||
2790            proposal.tx_hash.is_none() ||
2791            proposal.call_index.is_none()
2792        {
2793            return Err(Error::Custom(
2794                "[dao_vote] Proposal seems to not have been deployed yet".to_string(),
2795            ))
2796        }
2797
2798        // Check proposal is not executed
2799        if let Some(exec_tx_hash) = proposal.exec_tx_hash {
2800            return Err(Error::Custom(format!(
2801                "[dao_vote] Proposal was executed on transaction: {exec_tx_hash}"
2802            )))
2803        }
2804
2805        // Fetch DAO and check its deployed
2806        let Ok(dao) = self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await else {
2807            return Err(Error::Custom(format!(
2808                "[dao_vote] DAO {} was not found",
2809                proposal.proposal.dao_bulla
2810            )))
2811        };
2812        if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() {
2813            return Err(Error::Custom(
2814                "[dao_vote] DAO seems to not have been deployed yet".to_string(),
2815            ))
2816        }
2817
2818        // Fetch all the proposal votes to check for duplicate nullifiers
2819        let votes = self.get_dao_proposal_votes(proposal_bulla).await?;
2820        let mut votes_nullifiers = vec![];
2821        for vote in votes {
2822            for nullifier in vote.nullifiers {
2823                if !votes_nullifiers.contains(&nullifier) {
2824                    votes_nullifiers.push(nullifier);
2825                }
2826            }
2827        }
2828
2829        // Fetch our own governance OwnCoins to see what our balance is
2830        let gov_owncoins = self.get_token_coins(&dao.params.dao.gov_token_id).await?;
2831        if gov_owncoins.is_empty() {
2832            return Err(Error::Custom(format!(
2833                "[dao_vote] Did not find any governance {} coins in wallet",
2834                dao.params.dao.gov_token_id
2835            )))
2836        }
2837
2838        // Find which governance coins we can use
2839        let gov_owncoins_to_use = match weight {
2840            Some(_weight) => {
2841                // TODO: Build a proper coin selection algorithm so that we can use a
2842                // coins combination that matches the requested weight
2843                return Err(Error::Custom(
2844                    "[dao_vote] Fractional vote weight not supported yet".to_string(),
2845                ))
2846            }
2847            // If no weight was specified, use them all
2848            None => gov_owncoins,
2849        };
2850
2851        // Now we need to do a lookup for the zkas proof bincodes, and create
2852        // the circuit objects and proving keys so we can build the transaction.
2853        // We also do this through the RPC. First we grab the fee call from money.
2854        let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
2855
2856        let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1)
2857        else {
2858            return Err(Error::Custom("[dao_vote] Fee circuit not found".to_string()))
2859        };
2860
2861        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1, false)?;
2862
2863        let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
2864
2865        // Creating Fee circuit proving key
2866        let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
2867
2868        // Now we grab the DAO bins
2869        let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?;
2870
2871        let Some(dao_vote_burn_zkbin) =
2872            zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_VOTE_INPUT_NS)
2873        else {
2874            return Err(Error::Custom("[dao_vote] DAO Vote Burn circuit not found".to_string()))
2875        };
2876
2877        let Some(dao_vote_main_zkbin) =
2878            zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_VOTE_MAIN_NS)
2879        else {
2880            return Err(Error::Custom("[dao_vote] DAO Vote Main circuit not found".to_string()))
2881        };
2882
2883        let dao_vote_burn_zkbin = ZkBinary::decode(&dao_vote_burn_zkbin.1, false)?;
2884        let dao_vote_main_zkbin = ZkBinary::decode(&dao_vote_main_zkbin.1, false)?;
2885
2886        let dao_vote_burn_circuit =
2887            ZkCircuit::new(empty_witnesses(&dao_vote_burn_zkbin)?, &dao_vote_burn_zkbin);
2888        let dao_vote_main_circuit =
2889            ZkCircuit::new(empty_witnesses(&dao_vote_main_zkbin)?, &dao_vote_main_zkbin);
2890
2891        // Creating DAO VoteBurn and VoteMain circuits proving keys
2892        let dao_vote_burn_pk = ProvingKey::build(dao_vote_burn_zkbin.k, &dao_vote_burn_circuit);
2893        let dao_vote_main_pk = ProvingKey::build(dao_vote_main_zkbin.k, &dao_vote_main_circuit);
2894
2895        // Now create the parameters for the vote tx
2896        let mut inputs = Vec::with_capacity(gov_owncoins_to_use.len());
2897        for gov_owncoin in gov_owncoins_to_use {
2898            // Skip governance coins that are not part of the snapshot
2899            let Ok(merkle_path) = proposal
2900                .money_snapshot_tree
2901                .as_ref()
2902                .unwrap()
2903                .witness(gov_owncoin.leaf_position, 0)
2904            else {
2905                continue
2906            };
2907            let nullifier = poseidon_hash([gov_owncoin.secret.inner(), gov_owncoin.coin.inner()]);
2908            let vote_nullifier =
2909                poseidon_hash([nullifier, gov_owncoin.secret.inner(), proposal_bulla.inner()]);
2910            if votes_nullifiers.contains(&vote_nullifier.into()) {
2911                return Err(Error::Custom("[dao_vote] Duplicate input nullifier found".to_string()))
2912            };
2913
2914            let input = DaoVoteInput {
2915                secret: gov_owncoin.secret,
2916                note: gov_owncoin.note.clone(),
2917                leaf_position: gov_owncoin.leaf_position,
2918                merkle_path,
2919            };
2920            inputs.push(input);
2921        }
2922        if inputs.is_empty() {
2923            return Err(Error::Custom(format!(
2924                "[dao_vote] Did not find any governance {} coins in wallet before proposal snapshot",
2925                dao.params.dao.gov_token_id
2926            )))
2927        }
2928
2929        // Retrieve next block height and current block time target,
2930        // to compute their window.
2931        let next_block_height = self.get_next_block_height().await?;
2932        let block_target = self.get_block_target().await?;
2933        let current_blockwindow = blockwindow(next_block_height, block_target);
2934
2935        // Check if proposal has expired
2936        if current_blockwindow >=
2937            proposal.proposal.creation_blockwindow + proposal.proposal.duration_blockwindows
2938        {
2939            return Err(Error::Custom("[dao_vote] Proposal has expired".to_string()))
2940        }
2941
2942        // Generate the Money nullifiers Sparse Merkle Tree
2943        let store = MemoryStorageFp { tree: proposal.nullifiers_smt_snapshot.unwrap() };
2944        let money_null_smt = SmtMemoryFp::new(store, PoseidonFp::new(), &EMPTY_NODES_FP);
2945
2946        // Create the vote call
2947        let call = DaoVoteCall {
2948            money_null_smt: &money_null_smt,
2949            inputs,
2950            vote_option,
2951            proposal: proposal.proposal.clone(),
2952            dao: dao.params.dao.clone(),
2953            current_blockwindow,
2954        };
2955
2956        let (params, proofs, signature_secrets) = call.make(
2957            &dao_vote_burn_zkbin,
2958            &dao_vote_burn_pk,
2959            &dao_vote_main_zkbin,
2960            &dao_vote_main_pk,
2961        )?;
2962
2963        // Encode the call
2964        let mut data = vec![DaoFunction::Vote as u8];
2965        params.encode_async(&mut data).await?;
2966        let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
2967
2968        // Create the TransactionBuilder containing above call
2969        let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?;
2970
2971        // We first have to execute the fee-less tx to gather its used gas, and then we feed
2972        // it into the fee-creating function.
2973        let mut tx = tx_builder.build()?;
2974        let sigs = tx.create_sigs(&signature_secrets)?;
2975        tx.signatures.push(sigs);
2976
2977        let tree = self.get_money_tree().await?;
2978        let (fee_call, fee_proofs, fee_secrets) =
2979            self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?;
2980
2981        // Append the fee call to the transaction
2982        tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
2983
2984        // Now build the actual transaction and sign it with all necessary keys.
2985        let mut tx = tx_builder.build()?;
2986        let sigs = tx.create_sigs(&signature_secrets)?;
2987        tx.signatures.push(sigs);
2988        let sigs = tx.create_sigs(&fee_secrets)?;
2989        tx.signatures.push(sigs);
2990
2991        Ok(tx)
2992    }
2993
2994    /// Execute a DAO transfer proposal.
2995    pub async fn dao_exec_transfer(
2996        &self,
2997        proposal: &ProposalRecord,
2998        early: bool,
2999    ) -> Result<Transaction> {
3000        if proposal.leaf_position.is_none() ||
3001            proposal.money_snapshot_tree.is_none() ||
3002            proposal.nullifiers_smt_snapshot.is_none() ||
3003            proposal.tx_hash.is_none() ||
3004            proposal.call_index.is_none()
3005        {
3006            return Err(Error::Custom(
3007                "[dao_exec_transfer] Proposal seems to not have been deployed yet".to_string(),
3008            ))
3009        }
3010
3011        // Check proposal is not executed
3012        if let Some(exec_tx_hash) = proposal.exec_tx_hash {
3013            return Err(Error::Custom(format!(
3014                "[dao_exec_transfer] Proposal was executed on transaction: {exec_tx_hash}"
3015            )))
3016        }
3017
3018        // Check we know the plaintext data and they are valid
3019        if proposal.data.is_none() {
3020            return Err(Error::Custom(
3021                "[dao_exec_transfer] Proposal plainext data is empty".to_string(),
3022            ))
3023        }
3024        let proposal_coinattrs: CoinAttributes =
3025            deserialize_async(proposal.data.as_ref().unwrap()).await?;
3026
3027        // Fetch DAO and check its deployed
3028        let Ok(dao) = self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await else {
3029            return Err(Error::Custom(format!(
3030                "[dao_exec_transfer] DAO {} was not found",
3031                proposal.proposal.dao_bulla
3032            )))
3033        };
3034        if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() {
3035            return Err(Error::Custom(
3036                "[dao_exec_transfer] DAO seems to not have been deployed yet".to_string(),
3037            ))
3038        }
3039
3040        // Check that we have the exec key
3041        if dao.params.exec_secret_key.is_none() {
3042            return Err(Error::Custom(
3043                "[dao_exec_transfer] We need the exec secret key to execute proposals for this DAO"
3044                    .to_string(),
3045            ))
3046        }
3047
3048        // If early flag is provided, check that we have the early exec key
3049        if early && dao.params.early_exec_secret_key.is_none() {
3050            return Err(Error::Custom(
3051                "[dao_exec_transfer] We need the early exec secret key to execute proposals early for this DAO"
3052                    .to_string(),
3053            ))
3054        }
3055
3056        // Check proposal is approved
3057        let votes = self.get_dao_proposal_votes(&proposal.bulla()).await?;
3058        let mut yes_vote_value = 0;
3059        let mut yes_vote_blind = Blind::ZERO;
3060        let mut all_vote_value = 0;
3061        let mut all_vote_blind = Blind::ZERO;
3062        for vote in votes {
3063            if vote.vote_option {
3064                yes_vote_value += vote.all_vote_value;
3065            };
3066            yes_vote_blind += vote.yes_vote_blind;
3067            all_vote_value += vote.all_vote_value;
3068            all_vote_blind += vote.all_vote_blind;
3069        }
3070        let approval_ratio = (yes_vote_value as f64 * 100.0) / all_vote_value as f64;
3071        let dao_approval_ratio =
3072            (dao.params.dao.approval_ratio_quot / dao.params.dao.approval_ratio_base) as f64;
3073        if approval_ratio < dao_approval_ratio {
3074            return Err(Error::Custom(
3075                "[dao_exec_transfer] Proposal is not approved yet".to_string(),
3076            ))
3077        }
3078
3079        // Check the quorum has been met
3080        if all_vote_value < dao.params.dao.quorum {
3081            return Err(Error::Custom(
3082                "[dao_exec_transfer] DAO quorum is not reached yet".to_string(),
3083            ))
3084        }
3085        if early && all_vote_value < dao.params.dao.early_exec_quorum {
3086            return Err(Error::Custom(
3087                "[dao_exec_transfer] DAO early execution quorum is not reached yet".to_string(),
3088            ))
3089        }
3090
3091        // Fetch DAO unspent OwnCoins to see what its balance is for the coin
3092        let dao_spend_hook =
3093            FuncRef { contract_id: *DAO_CONTRACT_ID, func_code: DaoFunction::Exec as u8 }
3094                .to_func_id();
3095        let dao_owncoins = self
3096            .get_contract_token_coins(
3097                &proposal_coinattrs.token_id,
3098                &dao_spend_hook,
3099                &proposal.proposal.dao_bulla.inner(),
3100            )
3101            .await?;
3102        if dao_owncoins.is_empty() {
3103            return Err(Error::Custom(format!(
3104                "[dao_exec_transfer] Did not find any {} unspent coins owned by this DAO",
3105                proposal_coinattrs.token_id,
3106            )))
3107        }
3108
3109        // Check DAO balance is sufficient
3110        if dao_owncoins.iter().map(|x| x.note.value).sum::<u64>() < proposal_coinattrs.value {
3111            return Err(Error::Custom(format!(
3112                "[dao_exec_transfer] Not enough DAO balance for token ID: {}",
3113                proposal_coinattrs.token_id,
3114            )))
3115        }
3116
3117        // Find which DAO coins we can use
3118        let (spent_coins, change_value) = select_coins(dao_owncoins, proposal_coinattrs.value)?;
3119
3120        // Now we need to do a lookup for the zkas proof bincodes, and create
3121        // the circuit objects and proving keys so we can build the transaction.
3122        // We also do this through the RPC. First we grab the calls from money.
3123        let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
3124
3125        let Some(mint_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_MINT_NS_V1)
3126        else {
3127            return Err(Error::Custom("[dao_exec_transfer] Mint circuit not found".to_string()))
3128        };
3129
3130        let Some(burn_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_BURN_NS_V1)
3131        else {
3132            return Err(Error::Custom("[dao_exec_transfer] Burn circuit not found".to_string()))
3133        };
3134
3135        let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1)
3136        else {
3137            return Err(Error::Custom("[dao_exec_transfer] Fee circuit not found".to_string()))
3138        };
3139
3140        let mint_zkbin = ZkBinary::decode(&mint_zkbin.1, false)?;
3141        let burn_zkbin = ZkBinary::decode(&burn_zkbin.1, false)?;
3142        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1, false)?;
3143
3144        let mint_circuit = ZkCircuit::new(empty_witnesses(&mint_zkbin)?, &mint_zkbin);
3145        let burn_circuit = ZkCircuit::new(empty_witnesses(&burn_zkbin)?, &burn_zkbin);
3146        let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
3147
3148        // Creating Mint, Burn and Fee circuits proving keys
3149        let mint_pk = ProvingKey::build(mint_zkbin.k, &mint_circuit);
3150        let burn_pk = ProvingKey::build(burn_zkbin.k, &burn_circuit);
3151        let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
3152
3153        // Now we grab the DAO bins
3154        let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?;
3155
3156        let (namespace, early_exec_secret_key) = match early {
3157            true => {
3158                (DAO_CONTRACT_ZKAS_EARLY_EXEC_NS, Some(dao.params.early_exec_secret_key.unwrap()))
3159            }
3160            false => (DAO_CONTRACT_ZKAS_EXEC_NS, None),
3161        };
3162
3163        let Some(dao_exec_zkbin) = zkas_bins.iter().find(|x| x.0 == namespace) else {
3164            return Err(Error::Custom(format!(
3165                "[dao_exec_transfer] DAO {namespace} circuit not found"
3166            )))
3167        };
3168
3169        let Some(dao_auth_transfer_zkbin) =
3170            zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_AUTH_MONEY_TRANSFER_NS)
3171        else {
3172            return Err(Error::Custom(
3173                "[dao_exec_transfer] DAO AuthTransfer circuit not found".to_string(),
3174            ))
3175        };
3176
3177        let Some(dao_auth_transfer_enc_coin_zkbin) =
3178            zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_AUTH_MONEY_TRANSFER_ENC_COIN_NS)
3179        else {
3180            return Err(Error::Custom(
3181                "[dao_exec_transfer] DAO AuthTransferEncCoin circuit not found".to_string(),
3182            ))
3183        };
3184
3185        let dao_exec_zkbin = ZkBinary::decode(&dao_exec_zkbin.1, false)?;
3186        let dao_auth_transfer_zkbin = ZkBinary::decode(&dao_auth_transfer_zkbin.1, false)?;
3187        let dao_auth_transfer_enc_coin_zkbin =
3188            ZkBinary::decode(&dao_auth_transfer_enc_coin_zkbin.1, false)?;
3189
3190        let dao_exec_circuit = ZkCircuit::new(empty_witnesses(&dao_exec_zkbin)?, &dao_exec_zkbin);
3191        let dao_auth_transfer_circuit =
3192            ZkCircuit::new(empty_witnesses(&dao_auth_transfer_zkbin)?, &dao_auth_transfer_zkbin);
3193        let dao_auth_transfer_enc_coin_circuit = ZkCircuit::new(
3194            empty_witnesses(&dao_auth_transfer_enc_coin_zkbin)?,
3195            &dao_auth_transfer_enc_coin_zkbin,
3196        );
3197
3198        // Creating DAO Exec, AuthTransfer and AuthTransferEncCoin circuits proving keys
3199        let dao_exec_pk = ProvingKey::build(dao_exec_zkbin.k, &dao_exec_circuit);
3200        let dao_auth_transfer_pk =
3201            ProvingKey::build(dao_auth_transfer_zkbin.k, &dao_auth_transfer_circuit);
3202        let dao_auth_transfer_enc_coin_pk = ProvingKey::build(
3203            dao_auth_transfer_enc_coin_zkbin.k,
3204            &dao_auth_transfer_enc_coin_circuit,
3205        );
3206
3207        // Fetch our money Merkle tree
3208        let tree = self.get_money_tree().await?;
3209
3210        // Retrieve next block height and current block time target,
3211        // to compute their window.
3212        let next_block_height = self.get_next_block_height().await?;
3213        let block_target = self.get_block_target().await?;
3214        let current_blockwindow = blockwindow(next_block_height, block_target);
3215
3216        // Check if proposal duration has passed
3217        if !early &&
3218            current_blockwindow <
3219                proposal.proposal.creation_blockwindow +
3220                    proposal.proposal.duration_blockwindows
3221        {
3222            return Err(Error::Custom(
3223                "[dao_exec_transfer] Proposal period has not passed yet".to_string(),
3224            ))
3225        }
3226
3227        // Now we can create the transfer call parameters
3228        let input_user_data_blind = Blind::random(&mut OsRng);
3229        let mut inputs = vec![];
3230        for coin in &spent_coins {
3231            inputs.push(TransferCallInput {
3232                coin: coin.clone(),
3233                merkle_path: tree.witness(coin.leaf_position, 0).unwrap(),
3234                user_data_blind: input_user_data_blind,
3235            });
3236        }
3237
3238        let mut outputs = vec![];
3239        outputs.push(proposal_coinattrs.clone());
3240
3241        let dao_coin_attrs = CoinAttributes {
3242            public_key: dao.params.dao.notes_public_key,
3243            value: change_value,
3244            token_id: proposal_coinattrs.token_id,
3245            spend_hook: dao_spend_hook,
3246            user_data: proposal.proposal.dao_bulla.inner(),
3247            blind: Blind::random(&mut OsRng),
3248        };
3249        outputs.push(dao_coin_attrs.clone());
3250
3251        // Create the transfer call
3252        let transfer_builder = TransferCallBuilder {
3253            clear_inputs: vec![],
3254            inputs,
3255            outputs,
3256            mint_zkbin: mint_zkbin.clone(),
3257            mint_pk: mint_pk.clone(),
3258            burn_zkbin: burn_zkbin.clone(),
3259            burn_pk: burn_pk.clone(),
3260        };
3261        let (transfer_params, transfer_secrets) = transfer_builder.build()?;
3262
3263        // Encode the call
3264        let mut data = vec![MoneyFunction::TransferV1 as u8];
3265        transfer_params.encode_async(&mut data).await?;
3266        let transfer_call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
3267
3268        // Create the exec call
3269        let exec_signature_secret = SecretKey::random(&mut OsRng);
3270        let exec_builder = DaoExecCall {
3271            proposal: proposal.proposal.clone(),
3272            dao: dao.params.dao.clone(),
3273            yes_vote_value,
3274            all_vote_value,
3275            yes_vote_blind,
3276            all_vote_blind,
3277            signature_secret: exec_signature_secret,
3278            current_blockwindow,
3279        };
3280        let (exec_params, exec_proofs) = exec_builder.make(
3281            &dao.params.exec_secret_key.unwrap(),
3282            &early_exec_secret_key,
3283            &dao_exec_zkbin,
3284            &dao_exec_pk,
3285        )?;
3286
3287        // Encode the call
3288        let mut data = vec![DaoFunction::Exec as u8];
3289        exec_params.encode_async(&mut data).await?;
3290        let exec_call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
3291
3292        // Now we can create the auth call
3293        // Auth module
3294        let auth_transfer_builder = DaoAuthMoneyTransferCall {
3295            proposal: proposal.proposal.clone(),
3296            proposal_coinattrs: vec![proposal_coinattrs],
3297            dao: dao.params.dao.clone(),
3298            input_user_data_blind,
3299            dao_coin_attrs,
3300        };
3301        let (auth_transfer_params, auth_transfer_proofs) = auth_transfer_builder.make(
3302            &dao_auth_transfer_zkbin,
3303            &dao_auth_transfer_pk,
3304            &dao_auth_transfer_enc_coin_zkbin,
3305            &dao_auth_transfer_enc_coin_pk,
3306        )?;
3307
3308        // Encode the call
3309        let mut data = vec![DaoFunction::AuthMoneyTransfer as u8];
3310        auth_transfer_params.encode_async(&mut data).await?;
3311        let auth_transfer_call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
3312
3313        // Create the TransactionBuilder containing above calls
3314        let mut tx_builder = TransactionBuilder::new(
3315            ContractCallLeaf { call: exec_call, proofs: exec_proofs },
3316            vec![
3317                DarkTree::new(
3318                    ContractCallLeaf { call: auth_transfer_call, proofs: auth_transfer_proofs },
3319                    vec![],
3320                    None,
3321                    None,
3322                ),
3323                DarkTree::new(
3324                    ContractCallLeaf { call: transfer_call, proofs: transfer_secrets.proofs },
3325                    vec![],
3326                    None,
3327                    None,
3328                ),
3329            ],
3330        )?;
3331
3332        // We first have to execute the fee-less tx to gather its used gas, and then we feed
3333        // it into the fee-creating function.
3334        let mut tx = tx_builder.build()?;
3335        let auth_transfer_sigs = tx.create_sigs(&[])?;
3336        let transfer_sigs = tx.create_sigs(&transfer_secrets.signature_secrets)?;
3337        let exec_sigs = tx.create_sigs(&[exec_signature_secret])?;
3338        tx.signatures = vec![auth_transfer_sigs, transfer_sigs, exec_sigs];
3339
3340        let (fee_call, fee_proofs, fee_secrets) =
3341            self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?;
3342
3343        // Append the fee call to the transaction
3344        tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
3345
3346        // Now build the actual transaction and sign it with all necessary keys.
3347        let mut tx = tx_builder.build()?;
3348        let sigs = tx.create_sigs(&[])?;
3349        tx.signatures.push(sigs);
3350        let sigs = tx.create_sigs(&transfer_secrets.signature_secrets)?;
3351        tx.signatures.push(sigs);
3352        let sigs = tx.create_sigs(&[exec_signature_secret])?;
3353        tx.signatures.push(sigs);
3354        let sigs = tx.create_sigs(&fee_secrets)?;
3355        tx.signatures.push(sigs);
3356
3357        Ok(tx)
3358    }
3359
3360    /// Execute a DAO generic proposal.
3361    pub async fn dao_exec_generic(
3362        &self,
3363        proposal: &ProposalRecord,
3364        early: bool,
3365    ) -> Result<Transaction> {
3366        if proposal.leaf_position.is_none() ||
3367            proposal.money_snapshot_tree.is_none() ||
3368            proposal.nullifiers_smt_snapshot.is_none() ||
3369            proposal.tx_hash.is_none() ||
3370            proposal.call_index.is_none()
3371        {
3372            return Err(Error::Custom(
3373                "[dao_exec_generic] Proposal seems to not have been deployed yet".to_string(),
3374            ))
3375        }
3376
3377        // Check proposal is not executed
3378        if let Some(exec_tx_hash) = proposal.exec_tx_hash {
3379            return Err(Error::Custom(format!(
3380                "[dao_exec_generic] Proposal was executed on transaction: {exec_tx_hash}"
3381            )))
3382        }
3383
3384        // Fetch DAO and check its deployed
3385        let Ok(dao) = self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await else {
3386            return Err(Error::Custom(format!(
3387                "[dao_exec_generic] DAO {} was not found",
3388                proposal.proposal.dao_bulla
3389            )))
3390        };
3391        if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() {
3392            return Err(Error::Custom(
3393                "[dao_exec_generic] DAO seems to not have been deployed yet".to_string(),
3394            ))
3395        }
3396
3397        // Check that we have the exec key
3398        if dao.params.exec_secret_key.is_none() {
3399            return Err(Error::Custom(
3400                "[dao_exec_generic] We need the exec secret key to execute proposals for this DAO"
3401                    .to_string(),
3402            ))
3403        }
3404
3405        // If early flag is provided, check that we have the early exec key
3406        if early && dao.params.early_exec_secret_key.is_none() {
3407            return Err(Error::Custom(
3408                "[dao_exec_generic] We need the early exec secret key to execute proposals early for this DAO"
3409                    .to_string(),
3410            ))
3411        }
3412
3413        // Check proposal is approved
3414        let votes = self.get_dao_proposal_votes(&proposal.bulla()).await?;
3415        let mut yes_vote_value = 0;
3416        let mut yes_vote_blind = Blind::ZERO;
3417        let mut all_vote_value = 0;
3418        let mut all_vote_blind = Blind::ZERO;
3419        for vote in votes {
3420            if vote.vote_option {
3421                yes_vote_value += vote.all_vote_value;
3422            };
3423            yes_vote_blind += vote.yes_vote_blind;
3424            all_vote_value += vote.all_vote_value;
3425            all_vote_blind += vote.all_vote_blind;
3426        }
3427        let approval_ratio = (yes_vote_value as f64 * 100.0) / all_vote_value as f64;
3428        let dao_approval_ratio =
3429            (dao.params.dao.approval_ratio_quot / dao.params.dao.approval_ratio_base) as f64;
3430        if approval_ratio < dao_approval_ratio {
3431            return Err(Error::Custom("[dao_exec_generic] Proposal is not approved yet".to_string()))
3432        }
3433
3434        // Check the quorum has been met
3435        if all_vote_value < dao.params.dao.quorum {
3436            return Err(Error::Custom(
3437                "[dao_exec_generic] DAO quorum is not reached yet".to_string(),
3438            ))
3439        }
3440        if early && all_vote_value < dao.params.dao.early_exec_quorum {
3441            return Err(Error::Custom(
3442                "[dao_exec_generic] DAO early execution quorum is not reached yet".to_string(),
3443            ))
3444        }
3445
3446        // Now we need to do a lookup for the zkas proof bincodes, and create
3447        // the circuit objects and proving keys so we can build the transaction.
3448        // We also do this through the RPC. First we grab the calls from money.
3449        let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
3450        let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1)
3451        else {
3452            return Err(Error::Custom("[dao_exec_generic] Fee circuit not found".to_string()))
3453        };
3454        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1, false)?;
3455        let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
3456        let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
3457
3458        // Now we grab the DAO bins
3459        let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?;
3460
3461        let (namespace, early_exec_secret_key) = match early {
3462            true => {
3463                (DAO_CONTRACT_ZKAS_EARLY_EXEC_NS, Some(dao.params.early_exec_secret_key.unwrap()))
3464            }
3465            false => (DAO_CONTRACT_ZKAS_EXEC_NS, None),
3466        };
3467
3468        let Some(dao_exec_zkbin) = zkas_bins.iter().find(|x| x.0 == namespace) else {
3469            return Err(Error::Custom(format!(
3470                "[dao_exec_generic] DAO {namespace} circuit not found"
3471            )))
3472        };
3473        let dao_exec_zkbin = ZkBinary::decode(&dao_exec_zkbin.1, false)?;
3474        let dao_exec_circuit = ZkCircuit::new(empty_witnesses(&dao_exec_zkbin)?, &dao_exec_zkbin);
3475        let dao_exec_pk = ProvingKey::build(dao_exec_zkbin.k, &dao_exec_circuit);
3476
3477        // Fetch our money Merkle tree
3478        let tree = self.get_money_tree().await?;
3479
3480        // Retrieve next block height and current block time target,
3481        // to compute their window.
3482        let next_block_height = self.get_next_block_height().await?;
3483        let block_target = self.get_block_target().await?;
3484        let current_blockwindow = blockwindow(next_block_height, block_target);
3485
3486        // Check if proposal duration has passed
3487        if !early &&
3488            current_blockwindow <
3489                proposal.proposal.creation_blockwindow +
3490                    proposal.proposal.duration_blockwindows
3491        {
3492            return Err(Error::Custom(
3493                "[dao_exec_generic] Proposal period has not passed yet".to_string(),
3494            ))
3495        }
3496
3497        // Create the exec call
3498        let exec_signature_secret = SecretKey::random(&mut OsRng);
3499        let exec_builder = DaoExecCall {
3500            proposal: proposal.proposal.clone(),
3501            dao: dao.params.dao.clone(),
3502            yes_vote_value,
3503            all_vote_value,
3504            yes_vote_blind,
3505            all_vote_blind,
3506            signature_secret: exec_signature_secret,
3507            current_blockwindow,
3508        };
3509        let (exec_params, exec_proofs) = exec_builder.make(
3510            &dao.params.exec_secret_key.unwrap(),
3511            &early_exec_secret_key,
3512            &dao_exec_zkbin,
3513            &dao_exec_pk,
3514        )?;
3515
3516        // Encode the call
3517        let mut data = vec![DaoFunction::Exec as u8];
3518        exec_params.encode_async(&mut data).await?;
3519        let exec_call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
3520
3521        // Create the TransactionBuilder containing above calls
3522        let mut tx_builder = TransactionBuilder::new(
3523            ContractCallLeaf { call: exec_call, proofs: exec_proofs },
3524            vec![],
3525        )?;
3526
3527        // We first have to execute the fee-less tx to gather its used gas, and then we feed
3528        // it into the fee-creating function.
3529        let mut tx = tx_builder.build()?;
3530        let exec_sigs = tx.create_sigs(&[exec_signature_secret])?;
3531        tx.signatures = vec![exec_sigs];
3532
3533        let (fee_call, fee_proofs, fee_secrets) =
3534            self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?;
3535
3536        // Append the fee call to the transaction
3537        tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
3538
3539        // Now build the actual transaction and sign it with all necessary keys.
3540        let mut tx = tx_builder.build()?;
3541        let sigs = tx.create_sigs(&[exec_signature_secret])?;
3542        tx.signatures.push(sigs);
3543        let sigs = tx.create_sigs(&fee_secrets)?;
3544        tx.signatures.push(sigs);
3545
3546        Ok(tx)
3547    }
3548}