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