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