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 = \"{secret_key}\"\n\n"),
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 = \"{secret_key}\"\n\n"),
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 = \"{secret_key}\"\n\n"),
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 = \"{secret_key}\"\n\n"),
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 = \"{secret_key}\"\n\n"),
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
1202        let mut wallet_tx = false;
1203        for dao in &daos {
1204            if dao.bulla() != new_bulla {
1205                continue
1206            }
1207
1208            println!(
1209                "[apply_dao_mint_data] Found minted DAO {new_bulla}, noting down for wallet update"
1210            );
1211
1212            // We have this DAO imported in our wallet. Add the metadata:
1213            let mut dao_to_confirm = dao.clone();
1214            dao_to_confirm.leaf_position = daos_tree.mark();
1215            dao_to_confirm.tx_hash = Some(tx_hash);
1216            dao_to_confirm.call_index = Some(call_index);
1217
1218            // Confirm it
1219            if let Err(e) = self.confirm_dao(&dao_to_confirm).await {
1220                return Err(Error::DatabaseError(format!(
1221                    "[apply_dao_mint_data] Confirm DAO failed: {e:?}"
1222                )))
1223            }
1224
1225            wallet_tx = true;
1226            break
1227        }
1228
1229        // Update wallet data
1230        if let Err(e) = self.put_dao_trees(&daos_tree, &proposals_tree).await {
1231            return Err(Error::DatabaseError(format!(
1232                "[apply_dao_mint_data] Put DAO tree failed: {e:?}"
1233            )))
1234        }
1235
1236        Ok(wallet_tx)
1237    }
1238
1239    /// Auxiliary function to apply `DaoFunction::Propose` call data to the wallet,
1240    /// and store its inverse query into the cache.
1241    /// Returns a flag indicating if the provided call refers to our own wallet.
1242    async fn apply_dao_propose_data(
1243        &self,
1244        params: DaoProposeParams,
1245        tx_hash: TransactionHash,
1246        call_index: u8,
1247    ) -> Result<bool> {
1248        let daos = self.get_daos().await?;
1249        let (daos_tree, mut proposals_tree) = self.get_dao_trees().await?;
1250        proposals_tree.append(MerkleNode::from(params.proposal_bulla.inner()));
1251
1252        // If we're able to decrypt this note, that's the way to link it
1253        // to a specific DAO.
1254        let mut wallet_tx = false;
1255        for dao in &daos {
1256            // Check if we have the proposals key
1257            let Some(proposals_secret_key) = dao.params.proposals_secret_key else { continue };
1258
1259            // Try to decrypt the proposal note
1260            let Ok(note) = params.note.decrypt::<DaoProposal>(&proposals_secret_key) else {
1261                continue
1262            };
1263
1264            // We managed to decrypt it. Let's place this in a proper ProposalRecord object
1265            println!("[apply_dao_propose_data] Managed to decrypt DAO proposal note");
1266
1267            // We need to clone the trees here for reproducing the snapshot Merkle roots
1268            let money_tree = self.get_money_tree().await?;
1269            let nullifiers_smt = self.get_nullifiers_smt().await?;
1270
1271            // Check if we already got the record
1272            let our_proposal = match self.get_dao_proposal_by_bulla(&params.proposal_bulla).await {
1273                Ok(p) => {
1274                    let mut our_proposal = p;
1275                    our_proposal.leaf_position = proposals_tree.mark();
1276                    our_proposal.money_snapshot_tree = Some(money_tree);
1277                    our_proposal.nullifiers_smt_snapshot = Some(nullifiers_smt);
1278                    our_proposal.tx_hash = Some(tx_hash);
1279                    our_proposal.call_index = Some(call_index);
1280                    our_proposal
1281                }
1282                Err(_) => ProposalRecord {
1283                    proposal: note,
1284                    data: None,
1285                    leaf_position: proposals_tree.mark(),
1286                    money_snapshot_tree: Some(money_tree),
1287                    nullifiers_smt_snapshot: Some(nullifiers_smt),
1288                    tx_hash: Some(tx_hash),
1289                    call_index: Some(call_index),
1290                    exec_tx_hash: None,
1291                },
1292            };
1293
1294            // Update/store our record
1295            if let Err(e) = self.put_dao_proposal(&our_proposal).await {
1296                return Err(Error::DatabaseError(format!(
1297                    "[apply_dao_propose_data] Put DAO proposals failed: {e:?}"
1298                )))
1299            }
1300
1301            wallet_tx = true;
1302            break
1303        }
1304
1305        // Update wallet data
1306        if let Err(e) = self.put_dao_trees(&daos_tree, &proposals_tree).await {
1307            return Err(Error::DatabaseError(format!(
1308                "[apply_dao_propose_data] Put DAO tree failed: {e:?}"
1309            )))
1310        }
1311
1312        Ok(wallet_tx)
1313    }
1314
1315    /// Auxiliary function to apply `DaoFunction::Vote` call data to the wallet,
1316    /// and store its inverse query into the cache.
1317    /// Returns a flag indicating if the provided call refers to our own wallet.
1318    async fn apply_dao_vote_data(
1319        &self,
1320        params: DaoVoteParams,
1321        tx_hash: TransactionHash,
1322        call_index: u8,
1323    ) -> Result<bool> {
1324        // Check if we got the corresponding proposal
1325        let Ok(proposal) = self.get_dao_proposal_by_bulla(&params.proposal_bulla).await else {
1326            return Ok(false)
1327        };
1328
1329        // Grab the proposal DAO
1330        let dao = match self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await {
1331            Ok(d) => d,
1332            Err(e) => {
1333                return Err(Error::DatabaseError(format!(
1334                    "[apply_dao_vote_data] Couldn't find proposal {} DAO {}: {e}",
1335                    proposal.bulla(),
1336                    proposal.proposal.dao_bulla,
1337                )))
1338            }
1339        };
1340
1341        // Check if we have the votes key
1342        let Some(votes_secret_key) = dao.params.votes_secret_key else { return Ok(false) };
1343
1344        // Decrypt the vote note
1345        let note = match params.note.decrypt_unsafe(&votes_secret_key) {
1346            Ok(n) => n,
1347            Err(e) => {
1348                return Err(Error::DatabaseError(format!(
1349                    "[apply_dao_vote_data] Couldn't decrypt proposal {} vote with DAO {} keys: {e}",
1350                    proposal.bulla(),
1351                    proposal.proposal.dao_bulla,
1352                )))
1353            }
1354        };
1355
1356        // Create the DAO vote record
1357        let vote_option = fp_to_u64(note[0]).unwrap();
1358        if vote_option > 1 {
1359            return Err(Error::DatabaseError(format!(
1360                "[apply_dao_vote_data] Malformed vote for proposal {}: {vote_option}",
1361                proposal.bulla(),
1362            )))
1363        }
1364        let vote_option = vote_option != 0;
1365        let yes_vote_blind = Blind(fp_mod_fv(note[1]));
1366        let all_vote_value = fp_to_u64(note[2]).unwrap();
1367        let all_vote_blind = Blind(fp_mod_fv(note[3]));
1368
1369        let v = VoteRecord {
1370            id: 0, // This will be set by SQLite AUTOINCREMENT
1371            proposal: params.proposal_bulla,
1372            vote_option,
1373            yes_vote_blind,
1374            all_vote_value,
1375            all_vote_blind,
1376            tx_hash,
1377            call_index,
1378            nullifiers: params.inputs.iter().map(|i| i.vote_nullifier).collect(),
1379        };
1380
1381        if let Err(e) = self.put_dao_vote(&v).await {
1382            return Err(Error::DatabaseError(format!(
1383                "[apply_dao_vote_data] Put DAO votes failed: {e:?}"
1384            )))
1385        }
1386
1387        Ok(true)
1388    }
1389
1390    /// Auxiliary function to apply `DaoFunction::Exec` call data to the wallet,
1391    /// and store its inverse query into the cache.
1392    /// Returns a flag indicating if the provided call refers to our own wallet.
1393    async fn apply_dao_exec_data(
1394        &self,
1395        params: DaoExecParams,
1396        tx_hash: TransactionHash,
1397    ) -> Result<bool> {
1398        // Check if we got the corresponding proposal
1399        if self.get_dao_proposal_by_bulla(&params.proposal_bulla).await.is_err() {
1400            return Ok(false)
1401        };
1402
1403        // Grab proposal record key
1404        let key = serialize_async(&params.proposal_bulla).await;
1405
1406        // Create an SQL `UPDATE` query to update proposal exec transaction hash
1407        let query = format!(
1408            "UPDATE {} SET {} = ?1 WHERE {} = ?2;",
1409            *DAO_PROPOSALS_TABLE, DAO_PROPOSALS_COL_EXEC_TX_HASH, DAO_PROPOSALS_COL_BULLA,
1410        );
1411
1412        // Create its inverse query
1413        let inverse = match self.wallet.create_prepared_statement(
1414            &format!(
1415                "UPDATE {} SET {} = NULL WHERE {} = ?1;",
1416                *DAO_PROPOSALS_TABLE, DAO_PROPOSALS_COL_EXEC_TX_HASH, DAO_PROPOSALS_COL_BULLA,
1417            ),
1418            rusqlite::params![key],
1419        ) {
1420            Ok(q) => q,
1421            Err(e) => {
1422                return Err(Error::DatabaseError(format!(
1423                    "[apply_dao_exec_data] Creating DAO proposal update inverse query failed: {e:?}"
1424                )))
1425            }
1426        };
1427
1428        // Execute the query
1429        if let Err(e) = self
1430            .wallet
1431            .exec_sql(&query, rusqlite::params![Some(serialize_async(&tx_hash).await), key])
1432        {
1433            return Err(Error::DatabaseError(format!(
1434                "[apply_dao_exec_data] Update DAO proposal failed: {e:?}"
1435            )))
1436        }
1437
1438        // Store its inverse
1439        if let Err(e) = self.wallet.cache_inverse(inverse) {
1440            return Err(Error::DatabaseError(format!(
1441                "[apply_dao_exec_data] Inserting inverse query into cache failed: {e:?}"
1442            )))
1443        }
1444
1445        Ok(true)
1446    }
1447
1448    /// Append data related to DAO contract transactions into the wallet database,
1449    /// and store their inverse queries into the cache.
1450    /// Returns a flag indicating if the provided data refer to our own wallet.
1451    pub async fn apply_tx_dao_data(
1452        &self,
1453        data: &[u8],
1454        tx_hash: TransactionHash,
1455        call_idx: u8,
1456    ) -> Result<bool> {
1457        // Run through the transaction call data and see what we got:
1458        match DaoFunction::try_from(data[0])? {
1459            DaoFunction::Mint => {
1460                println!("[apply_tx_dao_data] Found Dao::Mint call");
1461                let params: DaoMintParams = deserialize_async(&data[1..]).await?;
1462                self.apply_dao_mint_data(params.dao_bulla, tx_hash, call_idx).await
1463            }
1464            DaoFunction::Propose => {
1465                println!("[apply_tx_dao_data] Found Dao::Propose call");
1466                let params: DaoProposeParams = deserialize_async(&data[1..]).await?;
1467                self.apply_dao_propose_data(params, tx_hash, call_idx).await
1468            }
1469            DaoFunction::Vote => {
1470                println!("[apply_tx_dao_data] Found Dao::Vote call");
1471                let params: DaoVoteParams = deserialize_async(&data[1..]).await?;
1472                self.apply_dao_vote_data(params, tx_hash, call_idx).await
1473            }
1474            DaoFunction::Exec => {
1475                println!("[apply_tx_dao_data] Found Dao::Exec call");
1476                let params: DaoExecParams = deserialize_async(&data[1..]).await?;
1477                self.apply_dao_exec_data(params, tx_hash).await
1478            }
1479            DaoFunction::AuthMoneyTransfer => {
1480                println!("[apply_tx_dao_data] Found Dao::AuthMoneyTransfer call");
1481                // Does nothing, just verifies the other calls are correct
1482                Ok(false)
1483            }
1484        }
1485    }
1486
1487    /// Confirm already imported DAO metadata into the wallet,
1488    /// and store its inverse query into the cache.
1489    /// Here we just write the leaf position, tx hash, and call index.
1490    /// Panics if the fields are None.
1491    pub async fn confirm_dao(&self, dao: &DaoRecord) -> WalletDbResult<()> {
1492        // Grab dao record key
1493        let key = serialize_async(&dao.bulla()).await;
1494
1495        // Create an SQL `UPDATE` query
1496        let query = format!(
1497            "UPDATE {} SET {} = ?1, {} = ?2, {} = ?3 WHERE {} = ?4;",
1498            *DAO_DAOS_TABLE,
1499            DAO_DAOS_COL_LEAF_POSITION,
1500            DAO_DAOS_COL_TX_HASH,
1501            DAO_DAOS_COL_CALL_INDEX,
1502            DAO_DAOS_COL_BULLA
1503        );
1504
1505        // Create its params
1506        let params = rusqlite::params![
1507            serialize_async(&dao.leaf_position.unwrap()).await,
1508            serialize_async(&dao.tx_hash.unwrap()).await,
1509            dao.call_index.unwrap(),
1510            key,
1511        ];
1512
1513        // Create its inverse query
1514        let inverse_query = format!(
1515            "UPDATE {} SET {} = NULL, {} = NULL, {} = NULL WHERE {} = ?1;",
1516            *DAO_DAOS_TABLE,
1517            DAO_DAOS_COL_LEAF_POSITION,
1518            DAO_DAOS_COL_TX_HASH,
1519            DAO_DAOS_COL_CALL_INDEX,
1520            DAO_DAOS_COL_BULLA
1521        );
1522        let inverse =
1523            self.wallet.create_prepared_statement(&inverse_query, rusqlite::params![key])?;
1524
1525        // Execute the query
1526        self.wallet.exec_sql(&query, params)?;
1527
1528        // Store its inverse
1529        self.wallet.cache_inverse(inverse)
1530    }
1531
1532    /// Import given DAO proposal into the wallet,
1533    /// and store its inverse query into the cache.
1534    pub async fn put_dao_proposal(&self, proposal: &ProposalRecord) -> Result<()> {
1535        // Check that we already have the proposal DAO
1536        if let Err(e) = self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await {
1537            return Err(Error::DatabaseError(format!(
1538                "[put_dao_proposal] Couldn't find proposal {} DAO {}: {e}",
1539                proposal.bulla(),
1540                proposal.proposal.dao_bulla
1541            )))
1542        }
1543
1544        // Grab proposal record key
1545        let key = serialize_async(&proposal.bulla()).await;
1546
1547        // Create an SQL `INSERT OR REPLACE` query
1548        let query = format!(
1549            "INSERT OR REPLACE INTO {} ({}, {}, {}, {}, {}, {}, {}, {}, {}, {}) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10);",
1550            *DAO_PROPOSALS_TABLE,
1551            DAO_PROPOSALS_COL_BULLA,
1552            DAO_PROPOSALS_COL_DAO_BULLA,
1553            DAO_PROPOSALS_COL_PROPOSAL,
1554            DAO_PROPOSALS_COL_DATA,
1555            DAO_PROPOSALS_COL_LEAF_POSITION,
1556            DAO_PROPOSALS_COL_MONEY_SNAPSHOT_TREE,
1557            DAO_PROPOSALS_COL_NULLIFIERS_SMT_SNAPSHOT,
1558            DAO_PROPOSALS_COL_TX_HASH,
1559            DAO_PROPOSALS_COL_CALL_INDEX,
1560            DAO_PROPOSALS_COL_EXEC_TX_HASH,
1561        );
1562
1563        // Create its params
1564        let data = match &proposal.data {
1565            Some(data) => Some(data),
1566            None => None,
1567        };
1568
1569        let leaf_position = match &proposal.leaf_position {
1570            Some(leaf_position) => Some(serialize_async(leaf_position).await),
1571            None => None,
1572        };
1573
1574        let money_snapshot_tree = match &proposal.money_snapshot_tree {
1575            Some(money_snapshot_tree) => Some(serialize_async(money_snapshot_tree).await),
1576            None => None,
1577        };
1578
1579        let nullifiers_smt_snapshot = match &proposal.nullifiers_smt_snapshot {
1580            Some(nullifiers_smt_snapshot) => Some(serialize_async(nullifiers_smt_snapshot).await),
1581            None => None,
1582        };
1583
1584        let tx_hash = match &proposal.tx_hash {
1585            Some(tx_hash) => Some(serialize_async(tx_hash).await),
1586            None => None,
1587        };
1588
1589        let exec_tx_hash = match &proposal.exec_tx_hash {
1590            Some(exec_tx_hash) => Some(serialize_async(exec_tx_hash).await),
1591            None => None,
1592        };
1593
1594        let params = rusqlite::params![
1595            key,
1596            serialize_async(&proposal.proposal.dao_bulla).await,
1597            serialize_async(&proposal.proposal).await,
1598            data,
1599            leaf_position,
1600            money_snapshot_tree,
1601            nullifiers_smt_snapshot,
1602            tx_hash,
1603            proposal.call_index,
1604            exec_tx_hash,
1605        ];
1606
1607        // Create its inverse query
1608        let inverse_query = format!(
1609            "UPDATE {} SET {} = NULL, {} = NULL, {} = NULL, {} = NULL, {} = NULL, {} = NULL WHERE {} = ?1;",
1610            *DAO_PROPOSALS_TABLE,
1611            DAO_PROPOSALS_COL_LEAF_POSITION,
1612            DAO_PROPOSALS_COL_MONEY_SNAPSHOT_TREE,
1613            DAO_PROPOSALS_COL_NULLIFIERS_SMT_SNAPSHOT,
1614            DAO_PROPOSALS_COL_TX_HASH,
1615            DAO_PROPOSALS_COL_CALL_INDEX,
1616            DAO_PROPOSALS_COL_EXEC_TX_HASH,
1617            DAO_PROPOSALS_COL_BULLA
1618        );
1619        let inverse =
1620            match self.wallet.create_prepared_statement(&inverse_query, rusqlite::params![key]) {
1621                Ok(q) => q,
1622                Err(e) => {
1623                    return Err(Error::DatabaseError(format!(
1624                    "[put_dao_proposal] Creating DAO proposal insert inverse query failed: {e:?}"
1625                )))
1626                }
1627            };
1628
1629        // Execute the query
1630        if let Err(e) = self.wallet.exec_sql(&query, params) {
1631            return Err(Error::DatabaseError(format!(
1632                "[put_dao_proposal] Proposal insert failed: {e:?}"
1633            )))
1634        };
1635
1636        // Store its inverse
1637        if let Err(e) = self.wallet.cache_inverse(inverse) {
1638            return Err(Error::DatabaseError(format!(
1639                "[put_dao_proposal] Inserting inverse query into cache failed: {e:?}"
1640            )))
1641        }
1642
1643        Ok(())
1644    }
1645
1646    /// Unconfirm imported DAO proposals by removing the leaf position, tx hash, and call index.
1647    pub async fn unconfirm_proposals(&self, proposals: &[ProposalRecord]) -> WalletDbResult<()> {
1648        for proposal in proposals {
1649            let query = format!(
1650                "UPDATE {} SET {} = NULL, {} = NULL, {} = NULL, {} = NULL, {} = NULL, {} = NULL WHERE {} = ?1;",
1651                *DAO_PROPOSALS_TABLE,
1652                DAO_PROPOSALS_COL_LEAF_POSITION,
1653                DAO_PROPOSALS_COL_MONEY_SNAPSHOT_TREE,
1654                DAO_PROPOSALS_COL_NULLIFIERS_SMT_SNAPSHOT,
1655                DAO_PROPOSALS_COL_TX_HASH,
1656                DAO_PROPOSALS_COL_CALL_INDEX,
1657                DAO_PROPOSALS_COL_EXEC_TX_HASH,
1658                DAO_PROPOSALS_COL_BULLA
1659            );
1660            self.wallet
1661                .exec_sql(&query, rusqlite::params![serialize_async(&proposal.bulla()).await])?;
1662        }
1663
1664        Ok(())
1665    }
1666
1667    /// Import given DAO vote into the wallet,
1668    /// and store its inverse query into the cache.
1669    pub async fn put_dao_vote(&self, vote: &VoteRecord) -> WalletDbResult<()> {
1670        println!("Importing DAO vote into wallet");
1671
1672        // Create an SQL `INSERT OR REPLACE` query
1673        let query = format!(
1674            "INSERT INTO {} ({}, {}, {}, {}, {}, {}, {}, {}) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8);",
1675            *DAO_VOTES_TABLE,
1676            DAO_VOTES_COL_PROPOSAL_BULLA,
1677            DAO_VOTES_COL_VOTE_OPTION,
1678            DAO_VOTES_COL_YES_VOTE_BLIND,
1679            DAO_VOTES_COL_ALL_VOTE_VALUE,
1680            DAO_VOTES_COL_ALL_VOTE_BLIND,
1681            DAO_VOTES_COL_TX_HASH,
1682            DAO_VOTES_COL_CALL_INDEX,
1683            DAO_VOTES_COL_NULLIFIERS,
1684        );
1685
1686        // Create its params
1687        let params = rusqlite::params![
1688            serialize_async(&vote.proposal).await,
1689            vote.vote_option as u64,
1690            serialize_async(&vote.yes_vote_blind).await,
1691            serialize_async(&vote.all_vote_value).await,
1692            serialize_async(&vote.all_vote_blind).await,
1693            serialize_async(&vote.tx_hash).await,
1694            vote.call_index,
1695            serialize_async(&vote.nullifiers).await,
1696        ];
1697
1698        // Create its inverse query.
1699        // Since we don't know the record ID we will remove it
1700        // using all its fields.
1701        let inverse_query = format!(
1702            "DELETE FROM {} WHERE {} = ?1 AND {} = ?2 AND {} = ?3 AND {} = ?4 AND {} = ?5 AND {} = ?6 AND {} = ?7 AND {} = ?8;",
1703            *DAO_VOTES_TABLE,
1704            DAO_VOTES_COL_PROPOSAL_BULLA,
1705            DAO_VOTES_COL_VOTE_OPTION,
1706            DAO_VOTES_COL_YES_VOTE_BLIND,
1707            DAO_VOTES_COL_ALL_VOTE_VALUE,
1708            DAO_VOTES_COL_ALL_VOTE_BLIND,
1709            DAO_VOTES_COL_TX_HASH,
1710            DAO_VOTES_COL_CALL_INDEX,
1711            DAO_VOTES_COL_NULLIFIERS,
1712        );
1713        let inverse = self.wallet.create_prepared_statement(&inverse_query, params)?;
1714
1715        // Execute the query
1716        self.wallet.exec_sql(&query, params)?;
1717
1718        // Store its inverse
1719        self.wallet.cache_inverse(inverse)?;
1720
1721        println!("DAO vote added to wallet");
1722
1723        Ok(())
1724    }
1725
1726    /// Reset the DAO Merkle trees in the wallet.
1727    pub async fn reset_dao_trees(&self) -> WalletDbResult<()> {
1728        println!("Resetting DAO Merkle trees");
1729        let tree = MerkleTree::new(1);
1730        self.put_dao_trees(&tree, &tree).await?;
1731        println!("Successfully reset DAO Merkle trees");
1732
1733        Ok(())
1734    }
1735
1736    /// Reset confirmed DAOs in the wallet.
1737    pub async fn reset_daos(&self) -> WalletDbResult<()> {
1738        println!("Resetting DAO confirmations");
1739        let query = format!(
1740            "UPDATE {} SET {} = NULL, {} = NULL, {} = NULL;",
1741            *DAO_DAOS_TABLE,
1742            DAO_DAOS_COL_LEAF_POSITION,
1743            DAO_DAOS_COL_TX_HASH,
1744            DAO_DAOS_COL_CALL_INDEX,
1745        );
1746        self.wallet.exec_sql(&query, &[])?;
1747        println!("Successfully unconfirmed DAOs");
1748
1749        Ok(())
1750    }
1751
1752    /// Reset all DAO proposals in the wallet.
1753    pub async fn reset_dao_proposals(&self) -> WalletDbResult<()> {
1754        println!("Resetting DAO proposals confirmations");
1755        let proposals = match self.get_proposals().await {
1756            Ok(p) => p,
1757            Err(e) => {
1758                println!("[reset_dao_proposals] DAO proposals retrieval failed: {e:?}");
1759                return Err(WalletDbError::GenericError);
1760            }
1761        };
1762        self.unconfirm_proposals(&proposals).await?;
1763        println!("Successfully unconfirmed DAO proposals");
1764
1765        Ok(())
1766    }
1767
1768    /// Reset all DAO votes in the wallet.
1769    pub fn reset_dao_votes(&self) -> WalletDbResult<()> {
1770        println!("Resetting DAO votes");
1771        let query = format!("DELETE FROM {};", *DAO_VOTES_TABLE);
1772        self.wallet.exec_sql(&query, &[])
1773    }
1774
1775    /// Import given DAO params into the wallet with a given name.
1776    pub async fn import_dao(&self, name: &str, params: &DaoParams) -> Result<()> {
1777        // First let's check if we've imported this DAO with the given name before.
1778        if self.get_dao_by_name(name).await.is_ok() {
1779            return Err(Error::DatabaseError(
1780                "[import_dao] This DAO has already been imported".to_string(),
1781            ))
1782        }
1783
1784        println!("Importing \"{name}\" DAO into the wallet");
1785
1786        let query = format!(
1787            "INSERT INTO {} ({}, {}, {}) VALUES (?1, ?2, ?3);",
1788            *DAO_DAOS_TABLE, DAO_DAOS_COL_BULLA, DAO_DAOS_COL_NAME, DAO_DAOS_COL_PARAMS,
1789        );
1790        if let Err(e) = self.wallet.exec_sql(
1791            &query,
1792            rusqlite::params![
1793                serialize_async(&params.dao.to_bulla()).await,
1794                name,
1795                serialize_async(params).await,
1796            ],
1797        ) {
1798            return Err(Error::DatabaseError(format!("[import_dao] DAO insert failed: {e:?}")))
1799        };
1800
1801        Ok(())
1802    }
1803
1804    /// Update given DAO params into the wallet, if the corresponding DAO exists.
1805    pub async fn update_dao_keys(&self, params: &DaoParams) -> Result<()> {
1806        // Grab the params DAO
1807        let bulla = params.dao.to_bulla();
1808        let Ok(dao) = self.get_dao_by_bulla(&bulla).await else {
1809            return Err(Error::DatabaseError(format!("[import_dao] DAO {bulla} was not found")))
1810        };
1811
1812        println!("Updating \"{}\" DAO keys into the wallet", dao.name);
1813
1814        let query = format!(
1815            "UPDATE {} SET {} = ?1 WHERE {} = ?2;",
1816            *DAO_DAOS_TABLE, DAO_DAOS_COL_PARAMS, DAO_DAOS_COL_BULLA,
1817        );
1818        if let Err(e) = self.wallet.exec_sql(
1819            &query,
1820            rusqlite::params![serialize_async(params).await, serialize_async(&bulla).await,],
1821        ) {
1822            return Err(Error::DatabaseError(format!("[update_dao_keys] DAO update failed: {e:?}")))
1823        };
1824
1825        Ok(())
1826    }
1827
1828    /// Fetch a DAO given its bulla.
1829    pub async fn get_dao_by_bulla(&self, bulla: &DaoBulla) -> Result<DaoRecord> {
1830        let row = match self.wallet.query_single(
1831            &DAO_DAOS_TABLE,
1832            &[],
1833            convert_named_params! {(DAO_DAOS_COL_BULLA, serialize_async(bulla).await)},
1834        ) {
1835            Ok(r) => r,
1836            Err(e) => {
1837                return Err(Error::DatabaseError(format!(
1838                    "[get_dao_by_bulla] DAO retrieval failed: {e:?}"
1839                )))
1840            }
1841        };
1842
1843        self.parse_dao_record(&row).await
1844    }
1845
1846    /// Fetch a DAO given its name.
1847    pub async fn get_dao_by_name(&self, name: &str) -> Result<DaoRecord> {
1848        let row = match self.wallet.query_single(
1849            &DAO_DAOS_TABLE,
1850            &[],
1851            convert_named_params! {(DAO_DAOS_COL_NAME, name)},
1852        ) {
1853            Ok(r) => r,
1854            Err(e) => {
1855                return Err(Error::DatabaseError(format!(
1856                    "[get_dao_by_name] DAO retrieval failed: {e:?}"
1857                )))
1858            }
1859        };
1860
1861        self.parse_dao_record(&row).await
1862    }
1863
1864    /// List DAO(s) imported in the wallet. If a name is given, just print the
1865    /// metadata for that specific one, if found.
1866    pub async fn dao_list(&self, name: &Option<String>) -> Result<()> {
1867        if let Some(name) = name {
1868            let dao = self.get_dao_by_name(name).await?;
1869            println!("{dao}");
1870            return Ok(());
1871        }
1872
1873        let daos = self.get_daos().await?;
1874        for (i, dao) in daos.iter().enumerate() {
1875            println!("{i}. {}", dao.name);
1876        }
1877
1878        Ok(())
1879    }
1880
1881    /// Fetch known unspent balances from the wallet for the given DAO name.
1882    pub async fn dao_balance(&self, name: &str) -> Result<HashMap<String, u64>> {
1883        let dao = self.get_dao_by_name(name).await?;
1884
1885        let dao_spend_hook =
1886            FuncRef { contract_id: *DAO_CONTRACT_ID, func_code: DaoFunction::Exec as u8 }
1887                .to_func_id();
1888
1889        let mut coins = self.get_coins(false).await?;
1890        coins.retain(|x| x.0.note.spend_hook == dao_spend_hook);
1891        coins.retain(|x| x.0.note.user_data == dao.bulla().inner());
1892
1893        // Fill this map with balances
1894        let mut balmap: HashMap<String, u64> = HashMap::new();
1895
1896        for coin in coins {
1897            let mut value = coin.0.note.value;
1898
1899            if let Some(prev) = balmap.get(&coin.0.note.token_id.to_string()) {
1900                value += prev;
1901            }
1902
1903            balmap.insert(coin.0.note.token_id.to_string(), value);
1904        }
1905
1906        Ok(balmap)
1907    }
1908
1909    /// Fetch all known DAO proposalss from the wallet.
1910    pub async fn get_proposals(&self) -> Result<Vec<ProposalRecord>> {
1911        let rows = match self.wallet.query_multiple(&DAO_PROPOSALS_TABLE, &[], &[]) {
1912            Ok(r) => r,
1913            Err(e) => {
1914                return Err(Error::DatabaseError(format!(
1915                    "[get_proposals] DAO proposalss retrieval failed: {e:?}"
1916                )))
1917            }
1918        };
1919
1920        let mut daos = Vec::with_capacity(rows.len());
1921        for row in rows {
1922            daos.push(self.parse_dao_proposal(&row).await?);
1923        }
1924
1925        Ok(daos)
1926    }
1927
1928    /// Fetch a DAO proposal by its bulla.
1929    pub async fn get_dao_proposal_by_bulla(
1930        &self,
1931        bulla: &DaoProposalBulla,
1932    ) -> Result<ProposalRecord> {
1933        // Grab the proposal record
1934        let row = match self.wallet.query_single(
1935            &DAO_PROPOSALS_TABLE,
1936            &[],
1937            convert_named_params! {(DAO_PROPOSALS_COL_BULLA, serialize_async(bulla).await)},
1938        ) {
1939            Ok(r) => r,
1940            Err(e) => {
1941                return Err(Error::DatabaseError(format!(
1942                    "[get_dao_proposal_by_bulla] DAO proposal retrieval failed: {e:?}"
1943                )))
1944            }
1945        };
1946
1947        // Parse rest of the record
1948        self.parse_dao_proposal(&row).await
1949    }
1950
1951    // Fetch all known DAO proposal votes from the wallet given a proposal ID.
1952    pub async fn get_dao_proposal_votes(
1953        &self,
1954        proposal: &DaoProposalBulla,
1955    ) -> Result<Vec<VoteRecord>> {
1956        let rows = match self.wallet.query_multiple(
1957            &DAO_VOTES_TABLE,
1958            &[],
1959            convert_named_params! {(DAO_VOTES_COL_PROPOSAL_BULLA, serialize_async(proposal).await)},
1960        ) {
1961            Ok(r) => r,
1962            Err(e) => {
1963                return Err(Error::DatabaseError(format!(
1964                    "[get_dao_proposal_votes] Votes retrieval failed: {e:?}"
1965                )))
1966            }
1967        };
1968
1969        let mut votes = Vec::with_capacity(rows.len());
1970        for row in rows {
1971            let Value::Integer(id) = row[0] else {
1972                return Err(Error::ParseFailed("[get_dao_proposal_votes] ID parsing failed"))
1973            };
1974            let Ok(id) = u64::try_from(id) else {
1975                return Err(Error::ParseFailed("[get_dao_proposal_votes] ID parsing failed"))
1976            };
1977
1978            let Value::Blob(ref proposal_bytes) = row[1] else {
1979                return Err(Error::ParseFailed(
1980                    "[get_dao_proposal_votes] Proposal bytes bytes parsing failed",
1981                ))
1982            };
1983            let proposal = deserialize_async(proposal_bytes).await?;
1984
1985            let Value::Integer(vote_option) = row[2] else {
1986                return Err(Error::ParseFailed(
1987                    "[get_dao_proposal_votes] Vote option parsing failed",
1988                ))
1989            };
1990            let Ok(vote_option) = u32::try_from(vote_option) else {
1991                return Err(Error::ParseFailed(
1992                    "[get_dao_proposal_votes] Vote option parsing failed",
1993                ))
1994            };
1995            let vote_option = vote_option != 0;
1996
1997            let Value::Blob(ref yes_vote_blind_bytes) = row[3] else {
1998                return Err(Error::ParseFailed(
1999                    "[get_dao_proposal_votes] Yes vote blind bytes parsing failed",
2000                ))
2001            };
2002            let yes_vote_blind = deserialize_async(yes_vote_blind_bytes).await?;
2003
2004            let Value::Blob(ref all_vote_value_bytes) = row[4] else {
2005                return Err(Error::ParseFailed(
2006                    "[get_dao_proposal_votes] All vote value bytes parsing failed",
2007                ))
2008            };
2009            let all_vote_value = deserialize_async(all_vote_value_bytes).await?;
2010
2011            let Value::Blob(ref all_vote_blind_bytes) = row[5] else {
2012                return Err(Error::ParseFailed(
2013                    "[get_dao_proposal_votes] All vote blind bytes parsing failed",
2014                ))
2015            };
2016            let all_vote_blind = deserialize_async(all_vote_blind_bytes).await?;
2017
2018            let Value::Blob(ref tx_hash_bytes) = row[6] else {
2019                return Err(Error::ParseFailed(
2020                    "[get_dao_proposal_votes] Transaction hash bytes parsing failed",
2021                ))
2022            };
2023            let tx_hash = deserialize_async(tx_hash_bytes).await?;
2024
2025            let Value::Integer(call_index) = row[7] else {
2026                return Err(Error::ParseFailed("[get_dao_proposal_votes] Call index parsing failed"))
2027            };
2028            let Ok(call_index) = u8::try_from(call_index) else {
2029                return Err(Error::ParseFailed("[get_dao_proposal_votes] Call index parsing failed"))
2030            };
2031
2032            let Value::Blob(ref nullifiers_bytes) = row[8] else {
2033                return Err(Error::ParseFailed(
2034                    "[get_dao_proposal_votes] Nullifiers bytes parsing failed",
2035                ))
2036            };
2037            let nullifiers = deserialize_async(nullifiers_bytes).await?;
2038
2039            let vote = VoteRecord {
2040                id,
2041                proposal,
2042                vote_option,
2043                yes_vote_blind,
2044                all_vote_value,
2045                all_vote_blind,
2046                tx_hash,
2047                call_index,
2048                nullifiers,
2049            };
2050
2051            votes.push(vote);
2052        }
2053
2054        Ok(votes)
2055    }
2056
2057    /// Mint a DAO on-chain.
2058    pub async fn dao_mint(&self, name: &str) -> Result<Transaction> {
2059        // Retrieve the dao record
2060        let dao = self.get_dao_by_name(name).await?;
2061
2062        // Check its not already minted
2063        if dao.tx_hash.is_some() {
2064            return Err(Error::Custom(
2065                "[dao_mint] This DAO seems to have already been minted on-chain".to_string(),
2066            ))
2067        }
2068
2069        // Check that we have all the keys
2070        if dao.params.notes_secret_key.is_none() ||
2071            dao.params.proposer_secret_key.is_none() ||
2072            dao.params.proposals_secret_key.is_none() ||
2073            dao.params.votes_secret_key.is_none() ||
2074            dao.params.exec_secret_key.is_none() ||
2075            dao.params.early_exec_secret_key.is_none()
2076        {
2077            return Err(Error::Custom(
2078                "[dao_mint] We need all the secrets key to mint the DAO on-chain".to_string(),
2079            ))
2080        }
2081
2082        // Now we need to do a lookup for the zkas proof bincodes, and create
2083        // the circuit objects and proving keys so we can build the transaction.
2084        // We also do this through the RPC. First we grab the fee call from money.
2085        let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
2086
2087        let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1)
2088        else {
2089            return Err(Error::Custom("Fee circuit not found".to_string()))
2090        };
2091
2092        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
2093
2094        let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
2095
2096        // Creating Fee circuit proving key
2097        let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
2098
2099        // Now we grab the DAO mint
2100        let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?;
2101
2102        let Some(dao_mint_zkbin) = zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_MINT_NS)
2103        else {
2104            return Err(Error::DatabaseError("[dao_mint] DAO Mint circuit not found".to_string()))
2105        };
2106
2107        let dao_mint_zkbin = ZkBinary::decode(&dao_mint_zkbin.1)?;
2108
2109        let dao_mint_circuit = ZkCircuit::new(empty_witnesses(&dao_mint_zkbin)?, &dao_mint_zkbin);
2110
2111        // Creating DAO Mint circuit proving key
2112        let dao_mint_pk = ProvingKey::build(dao_mint_zkbin.k, &dao_mint_circuit);
2113
2114        // Create the DAO mint call
2115        let notes_secret_key = dao.params.notes_secret_key.unwrap();
2116        let (params, proofs) = make_mint_call(
2117            &dao.params.dao,
2118            &notes_secret_key,
2119            &dao.params.proposer_secret_key.unwrap(),
2120            &dao.params.proposals_secret_key.unwrap(),
2121            &dao.params.votes_secret_key.unwrap(),
2122            &dao.params.exec_secret_key.unwrap(),
2123            &dao.params.early_exec_secret_key.unwrap(),
2124            &dao_mint_zkbin,
2125            &dao_mint_pk,
2126        )?;
2127        let mut data = vec![DaoFunction::Mint as u8];
2128        params.encode_async(&mut data).await?;
2129        let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
2130
2131        // Create the TransactionBuilder containing above call
2132        let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?;
2133
2134        // We first have to execute the fee-less tx to gather its used gas, and then we feed
2135        // it into the fee-creating function.
2136        let mut tx = tx_builder.build()?;
2137        let sigs = tx.create_sigs(&[notes_secret_key])?;
2138        tx.signatures.push(sigs);
2139
2140        let tree = self.get_money_tree().await?;
2141        let (fee_call, fee_proofs, fee_secrets) =
2142            self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?;
2143
2144        // Append the fee call to the transaction
2145        tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
2146
2147        // Now build the actual transaction and sign it with all necessary keys.
2148        let mut tx = tx_builder.build()?;
2149        let sigs = tx.create_sigs(&[notes_secret_key])?;
2150        tx.signatures.push(sigs);
2151        let sigs = tx.create_sigs(&fee_secrets)?;
2152        tx.signatures.push(sigs);
2153
2154        Ok(tx)
2155    }
2156
2157    /// Create a DAO transfer proposal.
2158    #[allow(clippy::too_many_arguments)]
2159    pub async fn dao_propose_transfer(
2160        &self,
2161        name: &str,
2162        duration_blockwindows: u64,
2163        amount: &str,
2164        token_id: TokenId,
2165        recipient: PublicKey,
2166        spend_hook: Option<FuncId>,
2167        user_data: Option<pallas::Base>,
2168    ) -> Result<ProposalRecord> {
2169        // Fetch DAO and check its deployed
2170        let dao = self.get_dao_by_name(name).await?;
2171        if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() {
2172            return Err(Error::Custom(
2173                "[dao_propose_transfer] DAO seems to not have been deployed yet".to_string(),
2174            ))
2175        }
2176
2177        // Check that we have the proposer key
2178        if dao.params.proposer_secret_key.is_none() {
2179            return Err(Error::Custom(
2180                "[dao_propose_transfer] We need the proposer secret key to create proposals for this DAO".to_string(),
2181            ))
2182        }
2183
2184        // Fetch DAO unspent OwnCoins to see what its balance is
2185        let dao_spend_hook =
2186            FuncRef { contract_id: *DAO_CONTRACT_ID, func_code: DaoFunction::Exec as u8 }
2187                .to_func_id();
2188        let dao_bulla = dao.bulla();
2189        let dao_owncoins =
2190            self.get_contract_token_coins(&token_id, &dao_spend_hook, &dao_bulla.inner()).await?;
2191        if dao_owncoins.is_empty() {
2192            return Err(Error::Custom(format!(
2193                "[dao_propose_transfer] Did not find any {token_id} unspent coins owned by this DAO"
2194            )))
2195        }
2196
2197        // Check DAO balance is sufficient
2198        let amount = decode_base10(amount, BALANCE_BASE10_DECIMALS, false)?;
2199        if dao_owncoins.iter().map(|x| x.note.value).sum::<u64>() < amount {
2200            return Err(Error::Custom(format!(
2201                "[dao_propose_transfer] Not enough DAO balance for token ID: {token_id}",
2202            )))
2203        }
2204
2205        // Generate proposal coin attributes
2206        let proposal_coinattrs = CoinAttributes {
2207            public_key: recipient,
2208            value: amount,
2209            token_id,
2210            spend_hook: spend_hook.unwrap_or(FuncId::none()),
2211            user_data: user_data.unwrap_or(pallas::Base::ZERO),
2212            blind: Blind::random(&mut OsRng),
2213        };
2214
2215        // Convert coin_params to actual coins
2216        let proposal_coins = vec![proposal_coinattrs.to_coin()];
2217        let mut proposal_data = vec![];
2218        proposal_coins.encode_async(&mut proposal_data).await?;
2219
2220        // Create Auth calls
2221        let auth_calls = vec![
2222            DaoAuthCall {
2223                contract_id: *DAO_CONTRACT_ID,
2224                function_code: DaoFunction::AuthMoneyTransfer as u8,
2225                auth_data: proposal_data,
2226            },
2227            DaoAuthCall {
2228                contract_id: *MONEY_CONTRACT_ID,
2229                function_code: MoneyFunction::TransferV1 as u8,
2230                auth_data: vec![],
2231            },
2232        ];
2233
2234        // Retrieve next block height and current block time target,
2235        // to compute their window.
2236        let next_block_height = self.get_next_block_height().await?;
2237        let block_target = self.get_block_target().await?;
2238        let creation_blockwindow = blockwindow(next_block_height, block_target);
2239
2240        // Create the actual proposal
2241        let proposal = DaoProposal {
2242            auth_calls,
2243            creation_blockwindow,
2244            duration_blockwindows,
2245            user_data: user_data.unwrap_or(pallas::Base::ZERO),
2246            dao_bulla,
2247            blind: Blind::random(&mut OsRng),
2248        };
2249
2250        let proposal_record = ProposalRecord {
2251            proposal,
2252            data: Some(serialize_async(&proposal_coinattrs).await),
2253            leaf_position: None,
2254            money_snapshot_tree: None,
2255            nullifiers_smt_snapshot: None,
2256            tx_hash: None,
2257            call_index: None,
2258            exec_tx_hash: None,
2259        };
2260
2261        if let Err(e) = self.put_dao_proposal(&proposal_record).await {
2262            return Err(Error::DatabaseError(format!(
2263                "[dao_propose_transfer] Put DAO proposal failed: {e:?}"
2264            )))
2265        }
2266
2267        Ok(proposal_record)
2268    }
2269
2270    /// Create a DAO generic proposal.
2271    pub async fn dao_propose_generic(
2272        &self,
2273        name: &str,
2274        duration_blockwindows: u64,
2275        user_data: Option<pallas::Base>,
2276    ) -> Result<ProposalRecord> {
2277        // Fetch DAO and check its deployed
2278        let dao = self.get_dao_by_name(name).await?;
2279        if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() {
2280            return Err(Error::Custom(
2281                "[dao_propose_generic] DAO seems to not have been deployed yet".to_string(),
2282            ))
2283        }
2284
2285        // Check that we have the proposer key
2286        if dao.params.proposer_secret_key.is_none() {
2287            return Err(Error::Custom(
2288                "[dao_propose_generic] We need the proposer secret key to create proposals for this DAO".to_string(),
2289            ))
2290        }
2291
2292        // Retrieve next block height and current block time target,
2293        // to compute their window.
2294        let next_block_height = self.get_next_block_height().await?;
2295        let block_target = self.get_block_target().await?;
2296        let creation_blockwindow = blockwindow(next_block_height, block_target);
2297
2298        // Create the actual proposal
2299        let proposal = DaoProposal {
2300            auth_calls: vec![],
2301            creation_blockwindow,
2302            duration_blockwindows,
2303            user_data: user_data.unwrap_or(pallas::Base::ZERO),
2304            dao_bulla: dao.bulla(),
2305            blind: Blind::random(&mut OsRng),
2306        };
2307
2308        let proposal_record = ProposalRecord {
2309            proposal,
2310            data: None,
2311            leaf_position: None,
2312            money_snapshot_tree: None,
2313            nullifiers_smt_snapshot: None,
2314            tx_hash: None,
2315            call_index: None,
2316            exec_tx_hash: None,
2317        };
2318
2319        if let Err(e) = self.put_dao_proposal(&proposal_record).await {
2320            return Err(Error::DatabaseError(format!(
2321                "[dao_propose_generic] Put DAO proposal failed: {e:?}"
2322            )))
2323        }
2324
2325        Ok(proposal_record)
2326    }
2327
2328    /// Create a DAO transfer proposal transaction.
2329    pub async fn dao_transfer_proposal_tx(&self, proposal: &ProposalRecord) -> Result<Transaction> {
2330        // Check we know the plaintext data
2331        if proposal.data.is_none() {
2332            return Err(Error::Custom(
2333                "[dao_transfer_proposal_tx] Proposal plainext data is empty".to_string(),
2334            ))
2335        }
2336        let proposal_coinattrs: CoinAttributes =
2337            deserialize_async(proposal.data.as_ref().unwrap()).await?;
2338
2339        // Fetch DAO and check its deployed
2340        let Ok(dao) = self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await else {
2341            return Err(Error::Custom(format!(
2342                "[dao_transfer_proposal_tx] DAO {} was not found",
2343                proposal.proposal.dao_bulla
2344            )))
2345        };
2346        if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() {
2347            return Err(Error::Custom(
2348                "[dao_transfer_proposal_tx] DAO seems to not have been deployed yet".to_string(),
2349            ))
2350        }
2351
2352        // Check that we have the proposer key
2353        if dao.params.proposer_secret_key.is_none() {
2354            return Err(Error::Custom(
2355                "[dao_transfer_proposal_tx] We need the proposer secret key to create proposals for this DAO".to_string(),
2356            ))
2357        }
2358
2359        // Fetch DAO unspent OwnCoins to see what its balance is for the coin
2360        let dao_spend_hook =
2361            FuncRef { contract_id: *DAO_CONTRACT_ID, func_code: DaoFunction::Exec as u8 }
2362                .to_func_id();
2363        let dao_owncoins = self
2364            .get_contract_token_coins(
2365                &proposal_coinattrs.token_id,
2366                &dao_spend_hook,
2367                &proposal.proposal.dao_bulla.inner(),
2368            )
2369            .await?;
2370        if dao_owncoins.is_empty() {
2371            return Err(Error::Custom(format!(
2372                "[dao_transfer_proposal_tx] Did not find any {} unspent coins owned by this DAO",
2373                proposal_coinattrs.token_id,
2374            )))
2375        }
2376
2377        // Check DAO balance is sufficient
2378        if dao_owncoins.iter().map(|x| x.note.value).sum::<u64>() < proposal_coinattrs.value {
2379            return Err(Error::Custom(format!(
2380                "[dao_transfer_proposal_tx] Not enough DAO balance for token ID: {}",
2381                proposal_coinattrs.token_id,
2382            )))
2383        }
2384
2385        // Fetch our own governance OwnCoins to see what our balance is
2386        let gov_owncoins = self.get_token_coins(&dao.params.dao.gov_token_id).await?;
2387        if gov_owncoins.is_empty() {
2388            return Err(Error::Custom(format!(
2389                "[dao_transfer_proposal_tx] Did not find any governance {} coins in wallet",
2390                dao.params.dao.gov_token_id
2391            )))
2392        }
2393
2394        // Find which governance coins we can use
2395        let mut total_value = 0;
2396        let mut gov_owncoins_to_use = vec![];
2397        for gov_owncoin in gov_owncoins {
2398            if total_value >= dao.params.dao.proposer_limit {
2399                break
2400            }
2401
2402            total_value += gov_owncoin.note.value;
2403            gov_owncoins_to_use.push(gov_owncoin);
2404        }
2405
2406        // Check our governance coins balance is sufficient
2407        if total_value < dao.params.dao.proposer_limit {
2408            return Err(Error::Custom(format!(
2409                "[dao_transfer_proposal_tx] Not enough gov token {} balance to propose",
2410                dao.params.dao.gov_token_id
2411            )))
2412        }
2413
2414        // Now we need to do a lookup for the zkas proof bincodes, and create
2415        // the circuit objects and proving keys so we can build the transaction.
2416        // We also do this through the RPC. First we grab the fee call from money.
2417        let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
2418
2419        let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1)
2420        else {
2421            return Err(Error::Custom(
2422                "[dao_transfer_proposal_tx] Fee circuit not found".to_string(),
2423            ))
2424        };
2425
2426        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
2427
2428        let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
2429
2430        // Creating Fee circuit proving key
2431        let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
2432
2433        // Now we grab the DAO bins
2434        let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?;
2435
2436        let Some(propose_burn_zkbin) =
2437            zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_PROPOSE_INPUT_NS)
2438        else {
2439            return Err(Error::Custom(
2440                "[dao_transfer_proposal_tx] Propose Burn circuit not found".to_string(),
2441            ))
2442        };
2443
2444        let Some(propose_main_zkbin) =
2445            zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_PROPOSE_MAIN_NS)
2446        else {
2447            return Err(Error::Custom(
2448                "[dao_transfer_proposal_tx] Propose Main circuit not found".to_string(),
2449            ))
2450        };
2451
2452        let propose_burn_zkbin = ZkBinary::decode(&propose_burn_zkbin.1)?;
2453        let propose_main_zkbin = ZkBinary::decode(&propose_main_zkbin.1)?;
2454
2455        let propose_burn_circuit =
2456            ZkCircuit::new(empty_witnesses(&propose_burn_zkbin)?, &propose_burn_zkbin);
2457        let propose_main_circuit =
2458            ZkCircuit::new(empty_witnesses(&propose_main_zkbin)?, &propose_main_zkbin);
2459
2460        // Creating DAO ProposeBurn and ProposeMain circuits proving keys
2461        let propose_burn_pk = ProvingKey::build(propose_burn_zkbin.k, &propose_burn_circuit);
2462        let propose_main_pk = ProvingKey::build(propose_main_zkbin.k, &propose_main_circuit);
2463
2464        // Fetch our money Merkle tree
2465        let money_merkle_tree = self.get_money_tree().await?;
2466
2467        // Now we can create the proposal transaction parameters.
2468        // We first generate the `DaoProposeStakeInput` inputs,
2469        // using our governance OwnCoins.
2470        let mut inputs = Vec::with_capacity(gov_owncoins_to_use.len());
2471        for gov_owncoin in gov_owncoins_to_use {
2472            let input = DaoProposeStakeInput {
2473                secret: gov_owncoin.secret,
2474                note: gov_owncoin.note.clone(),
2475                leaf_position: gov_owncoin.leaf_position,
2476                merkle_path: money_merkle_tree.witness(gov_owncoin.leaf_position, 0).unwrap(),
2477            };
2478            inputs.push(input);
2479        }
2480
2481        // Now create the parameters for the proposal tx
2482        let signature_secret = SecretKey::random(&mut OsRng);
2483
2484        // Fetch the daos Merkle tree to compute the DAO Merkle path and root
2485        let (daos_tree, _) = self.get_dao_trees().await?;
2486        let (dao_merkle_path, dao_merkle_root) = {
2487            let root = daos_tree.root(0).unwrap();
2488            let leaf_pos = dao.leaf_position.unwrap();
2489            let dao_merkle_path = daos_tree.witness(leaf_pos, 0).unwrap();
2490            (dao_merkle_path, root)
2491        };
2492
2493        // Generate the Money nullifiers Sparse Merkle Tree
2494        let store = WalletStorage::new(
2495            &self.wallet,
2496            &MONEY_SMT_TABLE,
2497            MONEY_SMT_COL_KEY,
2498            MONEY_SMT_COL_VALUE,
2499        );
2500        let money_null_smt = WalletSmt::new(store, PoseidonFp::new(), &EMPTY_NODES_FP);
2501
2502        // Create the proposal call
2503        let call = DaoProposeCall {
2504            money_null_smt: &money_null_smt,
2505            inputs,
2506            proposal: proposal.proposal.clone(),
2507            dao: dao.params.dao,
2508            dao_leaf_position: dao.leaf_position.unwrap(),
2509            dao_merkle_path,
2510            dao_merkle_root,
2511            signature_secret,
2512        };
2513
2514        let (params, proofs) = call.make(
2515            &dao.params.proposer_secret_key.unwrap(),
2516            &propose_burn_zkbin,
2517            &propose_burn_pk,
2518            &propose_main_zkbin,
2519            &propose_main_pk,
2520        )?;
2521
2522        // Encode the call
2523        let mut data = vec![DaoFunction::Propose as u8];
2524        params.encode_async(&mut data).await?;
2525        let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
2526
2527        // Create the TransactionBuilder containing above call
2528        let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?;
2529
2530        // We first have to execute the fee-less tx to gather its used gas, and then we feed
2531        // it into the fee-creating function.
2532        let mut tx = tx_builder.build()?;
2533        let sigs = tx.create_sigs(&[signature_secret])?;
2534        tx.signatures = vec![sigs];
2535
2536        let tree = self.get_money_tree().await?;
2537        let (fee_call, fee_proofs, fee_secrets) =
2538            self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?;
2539
2540        // Append the fee call to the transaction
2541        tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
2542
2543        // Now build the actual transaction and sign it with all necessary keys.
2544        let mut tx = tx_builder.build()?;
2545        let sigs = tx.create_sigs(&[signature_secret])?;
2546        tx.signatures.push(sigs);
2547        let sigs = tx.create_sigs(&fee_secrets)?;
2548        tx.signatures.push(sigs);
2549
2550        Ok(tx)
2551    }
2552
2553    /// Create a DAO generic proposal transaction.
2554    pub async fn dao_generic_proposal_tx(&self, proposal: &ProposalRecord) -> Result<Transaction> {
2555        // Fetch DAO and check its deployed
2556        let Ok(dao) = self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await else {
2557            return Err(Error::Custom(format!(
2558                "[dao_generic_proposal_tx] DAO {} was not found",
2559                proposal.proposal.dao_bulla
2560            )))
2561        };
2562        if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() {
2563            return Err(Error::Custom(
2564                "[dao_generic_proposal_tx] DAO seems to not have been deployed yet".to_string(),
2565            ))
2566        }
2567
2568        // Check that we have the proposer key
2569        if dao.params.proposer_secret_key.is_none() {
2570            return Err(Error::Custom(
2571                "[dao_generic_proposal_tx] We need the proposer secret key to create proposals for this DAO".to_string(),
2572            ))
2573        }
2574
2575        // Fetch our own governance OwnCoins to see what our balance is
2576        let gov_owncoins = self.get_token_coins(&dao.params.dao.gov_token_id).await?;
2577        if gov_owncoins.is_empty() {
2578            return Err(Error::Custom(format!(
2579                "[dao_generic_proposal_tx] Did not find any governance {} coins in wallet",
2580                dao.params.dao.gov_token_id
2581            )))
2582        }
2583
2584        // Find which governance coins we can use
2585        let mut total_value = 0;
2586        let mut gov_owncoins_to_use = vec![];
2587        for gov_owncoin in gov_owncoins {
2588            if total_value >= dao.params.dao.proposer_limit {
2589                break
2590            }
2591
2592            total_value += gov_owncoin.note.value;
2593            gov_owncoins_to_use.push(gov_owncoin);
2594        }
2595
2596        // Check our governance coins balance is sufficient
2597        if total_value < dao.params.dao.proposer_limit {
2598            return Err(Error::Custom(format!(
2599                "[dao_generic_proposal_tx] Not enough gov token {} balance to propose",
2600                dao.params.dao.gov_token_id
2601            )))
2602        }
2603
2604        // Now we need to do a lookup for the zkas proof bincodes, and create
2605        // the circuit objects and proving keys so we can build the transaction.
2606        // We also do this through the RPC. First we grab the fee call from money.
2607        let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
2608
2609        let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1)
2610        else {
2611            return Err(Error::Custom("[dao_generic_proposal_tx] Fee circuit not found".to_string()))
2612        };
2613
2614        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
2615
2616        let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
2617
2618        // Creating Fee circuit proving key
2619        let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
2620
2621        // Now we grab the DAO bins
2622        let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?;
2623
2624        let Some(propose_burn_zkbin) =
2625            zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_PROPOSE_INPUT_NS)
2626        else {
2627            return Err(Error::Custom(
2628                "[dao_generic_proposal_tx] Propose Burn circuit not found".to_string(),
2629            ))
2630        };
2631
2632        let Some(propose_main_zkbin) =
2633            zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_PROPOSE_MAIN_NS)
2634        else {
2635            return Err(Error::Custom(
2636                "[dao_generic_proposal_tx] Propose Main circuit not found".to_string(),
2637            ))
2638        };
2639
2640        let propose_burn_zkbin = ZkBinary::decode(&propose_burn_zkbin.1)?;
2641        let propose_main_zkbin = ZkBinary::decode(&propose_main_zkbin.1)?;
2642
2643        let propose_burn_circuit =
2644            ZkCircuit::new(empty_witnesses(&propose_burn_zkbin)?, &propose_burn_zkbin);
2645        let propose_main_circuit =
2646            ZkCircuit::new(empty_witnesses(&propose_main_zkbin)?, &propose_main_zkbin);
2647
2648        // Creating DAO ProposeBurn and ProposeMain circuits proving keys
2649        let propose_burn_pk = ProvingKey::build(propose_burn_zkbin.k, &propose_burn_circuit);
2650        let propose_main_pk = ProvingKey::build(propose_main_zkbin.k, &propose_main_circuit);
2651
2652        // Fetch our money Merkle tree
2653        let money_merkle_tree = self.get_money_tree().await?;
2654
2655        // Now we can create the proposal transaction parameters.
2656        // We first generate the `DaoProposeStakeInput` inputs,
2657        // using our governance OwnCoins.
2658        let mut inputs = Vec::with_capacity(gov_owncoins_to_use.len());
2659        for gov_owncoin in gov_owncoins_to_use {
2660            let input = DaoProposeStakeInput {
2661                secret: gov_owncoin.secret,
2662                note: gov_owncoin.note.clone(),
2663                leaf_position: gov_owncoin.leaf_position,
2664                merkle_path: money_merkle_tree.witness(gov_owncoin.leaf_position, 0).unwrap(),
2665            };
2666            inputs.push(input);
2667        }
2668
2669        // Now create the parameters for the proposal tx
2670        let signature_secret = SecretKey::random(&mut OsRng);
2671
2672        // Fetch the daos Merkle tree to compute the DAO Merkle path and root
2673        let (daos_tree, _) = self.get_dao_trees().await?;
2674        let (dao_merkle_path, dao_merkle_root) = {
2675            let root = daos_tree.root(0).unwrap();
2676            let leaf_pos = dao.leaf_position.unwrap();
2677            let dao_merkle_path = daos_tree.witness(leaf_pos, 0).unwrap();
2678            (dao_merkle_path, root)
2679        };
2680
2681        // Generate the Money nullifiers Sparse Merkle Tree
2682        let store = WalletStorage::new(
2683            &self.wallet,
2684            &MONEY_SMT_TABLE,
2685            MONEY_SMT_COL_KEY,
2686            MONEY_SMT_COL_VALUE,
2687        );
2688        let money_null_smt = WalletSmt::new(store, PoseidonFp::new(), &EMPTY_NODES_FP);
2689
2690        // Create the proposal call
2691        let call = DaoProposeCall {
2692            money_null_smt: &money_null_smt,
2693            inputs,
2694            proposal: proposal.proposal.clone(),
2695            dao: dao.params.dao,
2696            dao_leaf_position: dao.leaf_position.unwrap(),
2697            dao_merkle_path,
2698            dao_merkle_root,
2699            signature_secret,
2700        };
2701
2702        let (params, proofs) = call.make(
2703            &dao.params.proposer_secret_key.unwrap(),
2704            &propose_burn_zkbin,
2705            &propose_burn_pk,
2706            &propose_main_zkbin,
2707            &propose_main_pk,
2708        )?;
2709
2710        // Encode the call
2711        let mut data = vec![DaoFunction::Propose as u8];
2712        params.encode_async(&mut data).await?;
2713        let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
2714
2715        // Create the TransactionBuilder containing above call
2716        let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?;
2717
2718        // We first have to execute the fee-less tx to gather its used gas, and then we feed
2719        // it into the fee-creating function.
2720        let mut tx = tx_builder.build()?;
2721        let sigs = tx.create_sigs(&[signature_secret])?;
2722        tx.signatures = vec![sigs];
2723
2724        let tree = self.get_money_tree().await?;
2725        let (fee_call, fee_proofs, fee_secrets) =
2726            self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?;
2727
2728        // Append the fee call to the transaction
2729        tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
2730
2731        // Now build the actual transaction and sign it with all necessary keys.
2732        let mut tx = tx_builder.build()?;
2733        let sigs = tx.create_sigs(&[signature_secret])?;
2734        tx.signatures.push(sigs);
2735        let sigs = tx.create_sigs(&fee_secrets)?;
2736        tx.signatures.push(sigs);
2737
2738        Ok(tx)
2739    }
2740
2741    /// Vote on a DAO proposal
2742    pub async fn dao_vote(
2743        &self,
2744        proposal_bulla: &DaoProposalBulla,
2745        vote_option: bool,
2746        weight: Option<u64>,
2747    ) -> Result<Transaction> {
2748        // Feth the proposal and check its deployed
2749        let Ok(proposal) = self.get_dao_proposal_by_bulla(proposal_bulla).await else {
2750            return Err(Error::Custom(format!("[dao_vote] Proposal {proposal_bulla} was not found")))
2751        };
2752        if proposal.leaf_position.is_none() ||
2753            proposal.money_snapshot_tree.is_none() ||
2754            proposal.nullifiers_smt_snapshot.is_none() ||
2755            proposal.tx_hash.is_none() ||
2756            proposal.call_index.is_none()
2757        {
2758            return Err(Error::Custom(
2759                "[dao_vote] Proposal seems to not have been deployed yet".to_string(),
2760            ))
2761        }
2762
2763        // Check proposal is not executed
2764        if let Some(exec_tx_hash) = proposal.exec_tx_hash {
2765            return Err(Error::Custom(format!(
2766                "[dao_vote] Proposal was executed on transaction: {exec_tx_hash}"
2767            )))
2768        }
2769
2770        // Fetch DAO and check its deployed
2771        let Ok(dao) = self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await else {
2772            return Err(Error::Custom(format!(
2773                "[dao_vote] DAO {} was not found",
2774                proposal.proposal.dao_bulla
2775            )))
2776        };
2777        if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() {
2778            return Err(Error::Custom(
2779                "[dao_vote] DAO seems to not have been deployed yet".to_string(),
2780            ))
2781        }
2782
2783        // Fetch all the proposal votes to check for duplicate nullifiers
2784        let votes = self.get_dao_proposal_votes(proposal_bulla).await?;
2785        let mut votes_nullifiers = vec![];
2786        for vote in votes {
2787            for nullifier in vote.nullifiers {
2788                if !votes_nullifiers.contains(&nullifier) {
2789                    votes_nullifiers.push(nullifier);
2790                }
2791            }
2792        }
2793
2794        // Fetch our own governance OwnCoins to see what our balance is
2795        let gov_owncoins = self.get_token_coins(&dao.params.dao.gov_token_id).await?;
2796        if gov_owncoins.is_empty() {
2797            return Err(Error::Custom(format!(
2798                "[dao_vote] Did not find any governance {} coins in wallet",
2799                dao.params.dao.gov_token_id
2800            )))
2801        }
2802
2803        // Find which governance coins we can use
2804        let gov_owncoins_to_use = match weight {
2805            Some(_weight) => {
2806                // TODO: Build a proper coin selection algorithm so that we can use a
2807                // coins combination that matches the requested weight
2808                return Err(Error::Custom(
2809                    "[dao_vote] Fractional vote weight not supported yet".to_string(),
2810                ))
2811            }
2812            // If no weight was specified, use them all
2813            None => gov_owncoins,
2814        };
2815
2816        // Now we need to do a lookup for the zkas proof bincodes, and create
2817        // the circuit objects and proving keys so we can build the transaction.
2818        // We also do this through the RPC. First we grab the fee call from money.
2819        let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
2820
2821        let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1)
2822        else {
2823            return Err(Error::Custom("[dao_vote] Fee circuit not found".to_string()))
2824        };
2825
2826        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
2827
2828        let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
2829
2830        // Creating Fee circuit proving key
2831        let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
2832
2833        // Now we grab the DAO bins
2834        let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?;
2835
2836        let Some(dao_vote_burn_zkbin) =
2837            zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_VOTE_INPUT_NS)
2838        else {
2839            return Err(Error::Custom("[dao_vote] DAO Vote Burn circuit not found".to_string()))
2840        };
2841
2842        let Some(dao_vote_main_zkbin) =
2843            zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_VOTE_MAIN_NS)
2844        else {
2845            return Err(Error::Custom("[dao_vote] DAO Vote Main circuit not found".to_string()))
2846        };
2847
2848        let dao_vote_burn_zkbin = ZkBinary::decode(&dao_vote_burn_zkbin.1)?;
2849        let dao_vote_main_zkbin = ZkBinary::decode(&dao_vote_main_zkbin.1)?;
2850
2851        let dao_vote_burn_circuit =
2852            ZkCircuit::new(empty_witnesses(&dao_vote_burn_zkbin)?, &dao_vote_burn_zkbin);
2853        let dao_vote_main_circuit =
2854            ZkCircuit::new(empty_witnesses(&dao_vote_main_zkbin)?, &dao_vote_main_zkbin);
2855
2856        // Creating DAO VoteBurn and VoteMain circuits proving keys
2857        let dao_vote_burn_pk = ProvingKey::build(dao_vote_burn_zkbin.k, &dao_vote_burn_circuit);
2858        let dao_vote_main_pk = ProvingKey::build(dao_vote_main_zkbin.k, &dao_vote_main_circuit);
2859
2860        // Now create the parameters for the vote tx
2861        let signature_secret = SecretKey::random(&mut OsRng);
2862        let mut inputs = Vec::with_capacity(gov_owncoins_to_use.len());
2863        for gov_owncoin in gov_owncoins_to_use {
2864            let nullifier = poseidon_hash([gov_owncoin.secret.inner(), gov_owncoin.coin.inner()]);
2865            let vote_nullifier =
2866                poseidon_hash([nullifier, gov_owncoin.secret.inner(), proposal_bulla.inner()]);
2867            if votes_nullifiers.contains(&vote_nullifier.into()) {
2868                return Err(Error::Custom("[dao_vote] Duplicate input nullifier found".to_string()))
2869            };
2870
2871            let input = DaoVoteInput {
2872                secret: gov_owncoin.secret,
2873                note: gov_owncoin.note.clone(),
2874                leaf_position: gov_owncoin.leaf_position,
2875                merkle_path: proposal
2876                    .money_snapshot_tree
2877                    .as_ref()
2878                    .unwrap()
2879                    .witness(gov_owncoin.leaf_position, 0)
2880                    .unwrap(),
2881                signature_secret,
2882            };
2883            inputs.push(input);
2884        }
2885
2886        // Retrieve next block height and current block time target,
2887        // to compute their window.
2888        let next_block_height = self.get_next_block_height().await?;
2889        let block_target = self.get_block_target().await?;
2890        let current_blockwindow = blockwindow(next_block_height, block_target);
2891
2892        // Generate the Money nullifiers Sparse Merkle Tree
2893        let store = MemoryStorageFp { tree: proposal.nullifiers_smt_snapshot.unwrap() };
2894        let money_null_smt = SmtMemoryFp::new(store, PoseidonFp::new(), &EMPTY_NODES_FP);
2895
2896        // Create the vote call
2897        let call = DaoVoteCall {
2898            money_null_smt: &money_null_smt,
2899            inputs,
2900            vote_option,
2901            proposal: proposal.proposal.clone(),
2902            dao: dao.params.dao.clone(),
2903            current_blockwindow,
2904        };
2905
2906        let (params, proofs) = call.make(
2907            &dao_vote_burn_zkbin,
2908            &dao_vote_burn_pk,
2909            &dao_vote_main_zkbin,
2910            &dao_vote_main_pk,
2911        )?;
2912
2913        // Encode the call
2914        let mut data = vec![DaoFunction::Vote as u8];
2915        params.encode_async(&mut data).await?;
2916        let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
2917
2918        // Create the TransactionBuilder containing above call
2919        let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?;
2920
2921        // We first have to execute the fee-less tx to gather its used gas, and then we feed
2922        // it into the fee-creating function.
2923        let mut tx = tx_builder.build()?;
2924        let sigs = tx.create_sigs(&[signature_secret])?;
2925        tx.signatures = vec![sigs];
2926
2927        let tree = self.get_money_tree().await?;
2928        let (fee_call, fee_proofs, fee_secrets) =
2929            self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?;
2930
2931        // Append the fee call to the transaction
2932        tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
2933
2934        // Now build the actual transaction and sign it with all necessary keys.
2935        let mut tx = tx_builder.build()?;
2936        let sigs = tx.create_sigs(&[signature_secret])?;
2937        tx.signatures.push(sigs);
2938        let sigs = tx.create_sigs(&fee_secrets)?;
2939        tx.signatures.push(sigs);
2940
2941        Ok(tx)
2942    }
2943
2944    /// Execute a DAO transfer proposal.
2945    pub async fn dao_exec_transfer(
2946        &self,
2947        proposal: &ProposalRecord,
2948        early: bool,
2949    ) -> Result<Transaction> {
2950        if proposal.leaf_position.is_none() ||
2951            proposal.money_snapshot_tree.is_none() ||
2952            proposal.nullifiers_smt_snapshot.is_none() ||
2953            proposal.tx_hash.is_none() ||
2954            proposal.call_index.is_none()
2955        {
2956            return Err(Error::Custom(
2957                "[dao_exec_transfer] Proposal seems to not have been deployed yet".to_string(),
2958            ))
2959        }
2960
2961        // Check proposal is not executed
2962        if let Some(exec_tx_hash) = proposal.exec_tx_hash {
2963            return Err(Error::Custom(format!(
2964                "[dao_exec_transfer] Proposal was executed on transaction: {exec_tx_hash}"
2965            )))
2966        }
2967
2968        // Check we know the plaintext data and they are valid
2969        if proposal.data.is_none() {
2970            return Err(Error::Custom(
2971                "[dao_exec_transfer] Proposal plainext data is empty".to_string(),
2972            ))
2973        }
2974        let proposal_coinattrs: CoinAttributes =
2975            deserialize_async(proposal.data.as_ref().unwrap()).await?;
2976
2977        // Fetch DAO and check its deployed
2978        let Ok(dao) = self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await else {
2979            return Err(Error::Custom(format!(
2980                "[dao_exec_transfer] DAO {} was not found",
2981                proposal.proposal.dao_bulla
2982            )))
2983        };
2984        if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() {
2985            return Err(Error::Custom(
2986                "[dao_exec_transfer] DAO seems to not have been deployed yet".to_string(),
2987            ))
2988        }
2989
2990        // Check that we have the exec key
2991        if dao.params.exec_secret_key.is_none() {
2992            return Err(Error::Custom(
2993                "[dao_exec_transfer] We need the exec secret key to execute proposals for this DAO"
2994                    .to_string(),
2995            ))
2996        }
2997
2998        // If early flag is provided, check that we have the early exec key
2999        if early && dao.params.early_exec_secret_key.is_none() {
3000            return Err(Error::Custom(
3001                "[dao_exec_transfer] We need the early exec secret key to execute proposals early for this DAO"
3002                    .to_string(),
3003            ))
3004        }
3005
3006        // Check proposal is approved
3007        let votes = self.get_dao_proposal_votes(&proposal.bulla()).await?;
3008        let mut yes_vote_value = 0;
3009        let mut yes_vote_blind = Blind::ZERO;
3010        let mut all_vote_value = 0;
3011        let mut all_vote_blind = Blind::ZERO;
3012        for vote in votes {
3013            if vote.vote_option {
3014                yes_vote_value += vote.all_vote_value;
3015            };
3016            yes_vote_blind += vote.yes_vote_blind;
3017            all_vote_value += vote.all_vote_value;
3018            all_vote_blind += vote.all_vote_blind;
3019        }
3020        let approval_ratio = (yes_vote_value as f64 * 100.0) / all_vote_value as f64;
3021        if all_vote_value < dao.params.dao.quorum ||
3022            approval_ratio <
3023                (dao.params.dao.approval_ratio_quot / dao.params.dao.approval_ratio_base)
3024                    as f64
3025        {
3026            return Err(Error::Custom(
3027                "[dao_exec_transfer] Proposal is not approved yet".to_string(),
3028            ))
3029        };
3030
3031        // Fetch DAO unspent OwnCoins to see what its balance is for the coin
3032        let dao_spend_hook =
3033            FuncRef { contract_id: *DAO_CONTRACT_ID, func_code: DaoFunction::Exec as u8 }
3034                .to_func_id();
3035        let dao_owncoins = self
3036            .get_contract_token_coins(
3037                &proposal_coinattrs.token_id,
3038                &dao_spend_hook,
3039                &proposal.proposal.dao_bulla.inner(),
3040            )
3041            .await?;
3042        if dao_owncoins.is_empty() {
3043            return Err(Error::Custom(format!(
3044                "[dao_exec_transfer] Did not find any {} unspent coins owned by this DAO",
3045                proposal_coinattrs.token_id,
3046            )))
3047        }
3048
3049        // Check DAO balance is sufficient
3050        if dao_owncoins.iter().map(|x| x.note.value).sum::<u64>() < proposal_coinattrs.value {
3051            return Err(Error::Custom(format!(
3052                "[dao_exec_transfer] Not enough DAO balance for token ID: {}",
3053                proposal_coinattrs.token_id,
3054            )))
3055        }
3056
3057        // Find which DAO coins we can use
3058        let (spent_coins, change_value) = select_coins(dao_owncoins, proposal_coinattrs.value)?;
3059
3060        // Now we need to do a lookup for the zkas proof bincodes, and create
3061        // the circuit objects and proving keys so we can build the transaction.
3062        // We also do this through the RPC. First we grab the calls from money.
3063        let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
3064
3065        let Some(mint_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_MINT_NS_V1)
3066        else {
3067            return Err(Error::Custom("[dao_exec_transfer] Mint circuit not found".to_string()))
3068        };
3069
3070        let Some(burn_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_BURN_NS_V1)
3071        else {
3072            return Err(Error::Custom("[dao_exec_transfer] Burn circuit not found".to_string()))
3073        };
3074
3075        let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1)
3076        else {
3077            return Err(Error::Custom("[dao_exec_transfer] Fee circuit not found".to_string()))
3078        };
3079
3080        let mint_zkbin = ZkBinary::decode(&mint_zkbin.1)?;
3081        let burn_zkbin = ZkBinary::decode(&burn_zkbin.1)?;
3082        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
3083
3084        let mint_circuit = ZkCircuit::new(empty_witnesses(&mint_zkbin)?, &mint_zkbin);
3085        let burn_circuit = ZkCircuit::new(empty_witnesses(&burn_zkbin)?, &burn_zkbin);
3086        let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
3087
3088        // Creating Mint, Burn and Fee circuits proving keys
3089        let mint_pk = ProvingKey::build(mint_zkbin.k, &mint_circuit);
3090        let burn_pk = ProvingKey::build(burn_zkbin.k, &burn_circuit);
3091        let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
3092
3093        // Now we grab the DAO bins
3094        let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?;
3095
3096        let (namespace, early_exec_secret_key) = match early {
3097            true => (
3098                DAO_CONTRACT_ZKAS_DAO_EARLY_EXEC_NS,
3099                Some(dao.params.early_exec_secret_key.unwrap()),
3100            ),
3101            false => (DAO_CONTRACT_ZKAS_DAO_EXEC_NS, None),
3102        };
3103
3104        let Some(dao_exec_zkbin) = zkas_bins.iter().find(|x| x.0 == namespace) else {
3105            return Err(Error::Custom(format!(
3106                "[dao_exec_transfer] DAO {namespace} circuit not found"
3107            )))
3108        };
3109
3110        let Some(dao_auth_transfer_zkbin) =
3111            zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_AUTH_MONEY_TRANSFER_NS)
3112        else {
3113            return Err(Error::Custom(
3114                "[dao_exec_transfer] DAO AuthTransfer circuit not found".to_string(),
3115            ))
3116        };
3117
3118        let Some(dao_auth_transfer_enc_coin_zkbin) =
3119            zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_AUTH_MONEY_TRANSFER_ENC_COIN_NS)
3120        else {
3121            return Err(Error::Custom(
3122                "[dao_exec_transfer] DAO AuthTransferEncCoin circuit not found".to_string(),
3123            ))
3124        };
3125
3126        let dao_exec_zkbin = ZkBinary::decode(&dao_exec_zkbin.1)?;
3127        let dao_auth_transfer_zkbin = ZkBinary::decode(&dao_auth_transfer_zkbin.1)?;
3128        let dao_auth_transfer_enc_coin_zkbin =
3129            ZkBinary::decode(&dao_auth_transfer_enc_coin_zkbin.1)?;
3130
3131        let dao_exec_circuit = ZkCircuit::new(empty_witnesses(&dao_exec_zkbin)?, &dao_exec_zkbin);
3132        let dao_auth_transfer_circuit =
3133            ZkCircuit::new(empty_witnesses(&dao_auth_transfer_zkbin)?, &dao_auth_transfer_zkbin);
3134        let dao_auth_transfer_enc_coin_circuit = ZkCircuit::new(
3135            empty_witnesses(&dao_auth_transfer_enc_coin_zkbin)?,
3136            &dao_auth_transfer_enc_coin_zkbin,
3137        );
3138
3139        // Creating DAO Exec, AuthTransfer and AuthTransferEncCoin circuits proving keys
3140        let dao_exec_pk = ProvingKey::build(dao_exec_zkbin.k, &dao_exec_circuit);
3141        let dao_auth_transfer_pk =
3142            ProvingKey::build(dao_auth_transfer_zkbin.k, &dao_auth_transfer_circuit);
3143        let dao_auth_transfer_enc_coin_pk = ProvingKey::build(
3144            dao_auth_transfer_enc_coin_zkbin.k,
3145            &dao_auth_transfer_enc_coin_circuit,
3146        );
3147
3148        // Fetch our money Merkle tree
3149        let tree = self.get_money_tree().await?;
3150
3151        // Retrieve next block height and current block time target,
3152        // to compute their window.
3153        let next_block_height = self.get_next_block_height().await?;
3154        let block_target = self.get_block_target().await?;
3155        let current_blockwindow = blockwindow(next_block_height, block_target);
3156
3157        // Now we can create the transfer call parameters
3158        let input_user_data_blind = Blind::random(&mut OsRng);
3159        let mut inputs = vec![];
3160        for coin in &spent_coins {
3161            inputs.push(TransferCallInput {
3162                coin: coin.clone(),
3163                merkle_path: tree.witness(coin.leaf_position, 0).unwrap(),
3164                user_data_blind: input_user_data_blind,
3165            });
3166        }
3167
3168        let mut outputs = vec![];
3169        outputs.push(proposal_coinattrs.clone());
3170
3171        let dao_coin_attrs = CoinAttributes {
3172            public_key: dao.params.dao.notes_public_key,
3173            value: change_value,
3174            token_id: proposal_coinattrs.token_id,
3175            spend_hook: dao_spend_hook,
3176            user_data: proposal.proposal.dao_bulla.inner(),
3177            blind: Blind::random(&mut OsRng),
3178        };
3179        outputs.push(dao_coin_attrs.clone());
3180
3181        // Create the transfer call
3182        let transfer_builder = TransferCallBuilder {
3183            clear_inputs: vec![],
3184            inputs,
3185            outputs,
3186            mint_zkbin: mint_zkbin.clone(),
3187            mint_pk: mint_pk.clone(),
3188            burn_zkbin: burn_zkbin.clone(),
3189            burn_pk: burn_pk.clone(),
3190        };
3191        let (transfer_params, transfer_secrets) = transfer_builder.build()?;
3192
3193        // Encode the call
3194        let mut data = vec![MoneyFunction::TransferV1 as u8];
3195        transfer_params.encode_async(&mut data).await?;
3196        let transfer_call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
3197
3198        // Create the exec call
3199        let exec_signature_secret = SecretKey::random(&mut OsRng);
3200        let exec_builder = DaoExecCall {
3201            proposal: proposal.proposal.clone(),
3202            dao: dao.params.dao.clone(),
3203            yes_vote_value,
3204            all_vote_value,
3205            yes_vote_blind,
3206            all_vote_blind,
3207            signature_secret: exec_signature_secret,
3208            current_blockwindow,
3209        };
3210        let (exec_params, exec_proofs) = exec_builder.make(
3211            &dao.params.exec_secret_key.unwrap(),
3212            &early_exec_secret_key,
3213            &dao_exec_zkbin,
3214            &dao_exec_pk,
3215        )?;
3216
3217        // Encode the call
3218        let mut data = vec![DaoFunction::Exec as u8];
3219        exec_params.encode_async(&mut data).await?;
3220        let exec_call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
3221
3222        // Now we can create the auth call
3223        // Auth module
3224        let auth_transfer_builder = DaoAuthMoneyTransferCall {
3225            proposal: proposal.proposal.clone(),
3226            proposal_coinattrs: vec![proposal_coinattrs],
3227            dao: dao.params.dao.clone(),
3228            input_user_data_blind,
3229            dao_coin_attrs,
3230        };
3231        let (auth_transfer_params, auth_transfer_proofs) = auth_transfer_builder.make(
3232            &dao_auth_transfer_zkbin,
3233            &dao_auth_transfer_pk,
3234            &dao_auth_transfer_enc_coin_zkbin,
3235            &dao_auth_transfer_enc_coin_pk,
3236        )?;
3237
3238        // Encode the call
3239        let mut data = vec![DaoFunction::AuthMoneyTransfer as u8];
3240        auth_transfer_params.encode_async(&mut data).await?;
3241        let auth_transfer_call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
3242
3243        // Create the TransactionBuilder containing above calls
3244        let mut tx_builder = TransactionBuilder::new(
3245            ContractCallLeaf { call: exec_call, proofs: exec_proofs },
3246            vec![
3247                DarkTree::new(
3248                    ContractCallLeaf { call: auth_transfer_call, proofs: auth_transfer_proofs },
3249                    vec![],
3250                    None,
3251                    None,
3252                ),
3253                DarkTree::new(
3254                    ContractCallLeaf { call: transfer_call, proofs: transfer_secrets.proofs },
3255                    vec![],
3256                    None,
3257                    None,
3258                ),
3259            ],
3260        )?;
3261
3262        // We first have to execute the fee-less tx to gather its used gas, and then we feed
3263        // it into the fee-creating function.
3264        let mut tx = tx_builder.build()?;
3265        let auth_transfer_sigs = tx.create_sigs(&[])?;
3266        let transfer_sigs = tx.create_sigs(&transfer_secrets.signature_secrets)?;
3267        let exec_sigs = tx.create_sigs(&[exec_signature_secret])?;
3268        tx.signatures = vec![auth_transfer_sigs, transfer_sigs, exec_sigs];
3269
3270        let (fee_call, fee_proofs, fee_secrets) =
3271            self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?;
3272
3273        // Append the fee call to the transaction
3274        tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
3275
3276        // Now build the actual transaction and sign it with all necessary keys.
3277        let mut tx = tx_builder.build()?;
3278        let sigs = tx.create_sigs(&[])?;
3279        tx.signatures.push(sigs);
3280        let sigs = tx.create_sigs(&transfer_secrets.signature_secrets)?;
3281        tx.signatures.push(sigs);
3282        let sigs = tx.create_sigs(&[exec_signature_secret])?;
3283        tx.signatures.push(sigs);
3284        let sigs = tx.create_sigs(&fee_secrets)?;
3285        tx.signatures.push(sigs);
3286
3287        Ok(tx)
3288    }
3289
3290    /// Execute a DAO generic proposal.
3291    pub async fn dao_exec_generic(
3292        &self,
3293        proposal: &ProposalRecord,
3294        early: bool,
3295    ) -> Result<Transaction> {
3296        if proposal.leaf_position.is_none() ||
3297            proposal.money_snapshot_tree.is_none() ||
3298            proposal.nullifiers_smt_snapshot.is_none() ||
3299            proposal.tx_hash.is_none() ||
3300            proposal.call_index.is_none()
3301        {
3302            return Err(Error::Custom(
3303                "[dao_exec_generic] Proposal seems to not have been deployed yet".to_string(),
3304            ))
3305        }
3306
3307        // Check proposal is not executed
3308        if let Some(exec_tx_hash) = proposal.exec_tx_hash {
3309            return Err(Error::Custom(format!(
3310                "[dao_exec_generic] Proposal was executed on transaction: {exec_tx_hash}"
3311            )))
3312        }
3313
3314        // Fetch DAO and check its deployed
3315        let Ok(dao) = self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await else {
3316            return Err(Error::Custom(format!(
3317                "[dao_exec_generic] DAO {} was not found",
3318                proposal.proposal.dao_bulla
3319            )))
3320        };
3321        if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() {
3322            return Err(Error::Custom(
3323                "[dao_exec_generic] DAO seems to not have been deployed yet".to_string(),
3324            ))
3325        }
3326
3327        // Check that we have the exec key
3328        if dao.params.exec_secret_key.is_none() {
3329            return Err(Error::Custom(
3330                "[dao_exec_generic] We need the exec secret key to execute proposals for this DAO"
3331                    .to_string(),
3332            ))
3333        }
3334
3335        // If early flag is provided, check that we have the early exec key
3336        if early && dao.params.early_exec_secret_key.is_none() {
3337            return Err(Error::Custom(
3338                "[dao_exec_generic] We need the early exec secret key to execute proposals early for this DAO"
3339                    .to_string(),
3340            ))
3341        }
3342
3343        // Check proposal is approved
3344        let votes = self.get_dao_proposal_votes(&proposal.bulla()).await?;
3345        let mut yes_vote_value = 0;
3346        let mut yes_vote_blind = Blind::ZERO;
3347        let mut all_vote_value = 0;
3348        let mut all_vote_blind = Blind::ZERO;
3349        for vote in votes {
3350            if vote.vote_option {
3351                yes_vote_value += vote.all_vote_value;
3352            };
3353            yes_vote_blind += vote.yes_vote_blind;
3354            all_vote_value += vote.all_vote_value;
3355            all_vote_blind += vote.all_vote_blind;
3356        }
3357        let approval_ratio = (yes_vote_value as f64 * 100.0) / all_vote_value as f64;
3358        if all_vote_value < dao.params.dao.quorum ||
3359            approval_ratio <
3360                (dao.params.dao.approval_ratio_quot / dao.params.dao.approval_ratio_base)
3361                    as f64
3362        {
3363            return Err(Error::Custom("[dao_exec_generic] Proposal is not approved yet".to_string()))
3364        };
3365
3366        // Now we need to do a lookup for the zkas proof bincodes, and create
3367        // the circuit objects and proving keys so we can build the transaction.
3368        // We also do this through the RPC. First we grab the calls from money.
3369        let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
3370        let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1)
3371        else {
3372            return Err(Error::Custom("[dao_exec_generic] Fee circuit not found".to_string()))
3373        };
3374        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
3375        let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
3376        let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
3377
3378        // Now we grab the DAO bins
3379        let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?;
3380
3381        let (namespace, early_exec_secret_key) = match early {
3382            true => (
3383                DAO_CONTRACT_ZKAS_DAO_EARLY_EXEC_NS,
3384                Some(dao.params.early_exec_secret_key.unwrap()),
3385            ),
3386            false => (DAO_CONTRACT_ZKAS_DAO_EXEC_NS, None),
3387        };
3388
3389        let Some(dao_exec_zkbin) = zkas_bins.iter().find(|x| x.0 == namespace) else {
3390            return Err(Error::Custom(format!(
3391                "[dao_exec_generic] DAO {namespace} circuit not found"
3392            )))
3393        };
3394        let dao_exec_zkbin = ZkBinary::decode(&dao_exec_zkbin.1)?;
3395        let dao_exec_circuit = ZkCircuit::new(empty_witnesses(&dao_exec_zkbin)?, &dao_exec_zkbin);
3396        let dao_exec_pk = ProvingKey::build(dao_exec_zkbin.k, &dao_exec_circuit);
3397
3398        // Fetch our money Merkle tree
3399        let tree = self.get_money_tree().await?;
3400
3401        // Retrieve next block height and current block time target,
3402        // to compute their window.
3403        let next_block_height = self.get_next_block_height().await?;
3404        let block_target = self.get_block_target().await?;
3405        let current_blockwindow = blockwindow(next_block_height, block_target);
3406
3407        // Create the exec call
3408        let exec_signature_secret = SecretKey::random(&mut OsRng);
3409        let exec_builder = DaoExecCall {
3410            proposal: proposal.proposal.clone(),
3411            dao: dao.params.dao.clone(),
3412            yes_vote_value,
3413            all_vote_value,
3414            yes_vote_blind,
3415            all_vote_blind,
3416            signature_secret: exec_signature_secret,
3417            current_blockwindow,
3418        };
3419        let (exec_params, exec_proofs) = exec_builder.make(
3420            &dao.params.exec_secret_key.unwrap(),
3421            &early_exec_secret_key,
3422            &dao_exec_zkbin,
3423            &dao_exec_pk,
3424        )?;
3425
3426        // Encode the call
3427        let mut data = vec![DaoFunction::Exec as u8];
3428        exec_params.encode_async(&mut data).await?;
3429        let exec_call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
3430
3431        // Create the TransactionBuilder containing above calls
3432        let mut tx_builder = TransactionBuilder::new(
3433            ContractCallLeaf { call: exec_call, proofs: exec_proofs },
3434            vec![],
3435        )?;
3436
3437        // We first have to execute the fee-less tx to gather its used gas, and then we feed
3438        // it into the fee-creating function.
3439        let mut tx = tx_builder.build()?;
3440        let exec_sigs = tx.create_sigs(&[exec_signature_secret])?;
3441        tx.signatures = vec![exec_sigs];
3442
3443        let (fee_call, fee_proofs, fee_secrets) =
3444            self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?;
3445
3446        // Append the fee call to the transaction
3447        tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
3448
3449        // Now build the actual transaction and sign it with all necessary keys.
3450        let mut tx = tx_builder.build()?;
3451        let sigs = tx.create_sigs(&[exec_signature_secret])?;
3452        tx.signatures.push(sigs);
3453        let sigs = tx.create_sigs(&fee_secrets)?;
3454        tx.signatures.push(sigs);
3455
3456        Ok(tx)
3457    }
3458}