drk/
dao.rs

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